我是这样使用SpringBoot(多认证方式)

目录

有的时候,我们需要为用户提供多种认证方式。如:用户名密码登录、手术号验证码登录。下面实现Spring Security支持这两种登录方式。

增加Token

Spring Security默认使用UsernamePasswordAuthenticationToken包装登录请求的信息,Token继承之AbstractAuthenticationToken。Spring Security将信息封装成Token交给Provider处理。
这里增加一个MobileCodeAuthenticationToken类,继承之AbstractAuthenticationToken。用于封装手机号验证码的请求参数。后面会有相应的Provider处理这个Token。
创建包com.biboheart.demos.security.tokens,在包下创建类MobileCodeAuthenticationToken。可以查看UsernamePasswordAuthenticationToken类源码,参考它完成类的代码,内容如下:

package com.biboheart.demos.security.tokens;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

import java.util.Collection;

public class MobileCodeAuthenticationToken extends AbstractAuthenticationToken {
    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;
    private String credentials;

    public MobileCodeAuthenticationToken(Object principal, String credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }

    public MobileCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = null;
        super.setAuthenticated(true); // must use super, as we override
    }

    @Override
    public String getCredentials() {
        return this.credentials;
    }

    @Override
    public Object getPrincipal() {
        return this.principal;
    }

    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
        if (isAuthenticated) {
            throw new IllegalArgumentException(
                    "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        }

        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials() {
        super.eraseCredentials();
        credentials = null;
    }
}

Provider

Provider实现AuthenticationProvider接口,它执行身份认证工作。前面用的是Spring Security默认的Provider进行认证,我们没有控制认证过程。在这里我们实现两个Provider,UsernamePasswordAuthenticationProvider用于替换系统默认的用户名密码认证业务,MobileCodeAuthenticationProvider用于执行手机号验证码认证业务。这两个类创建在包com.biboheart.demos.security.provider下。实现接口AuthenticationProvider,其中Authentication authenticate函数用于执行认证,supports函数用于筛选Token,如果在这里返回true,所有Token都会认证。

package com.biboheart.demos.security.provider;

import com.biboheart.brick.utils.CheckUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.HashSet;
import java.util.Set;

public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider {
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
        String password = (String) authentication.getCredentials();
        // 认证用户名
        if (!"user".equals(username) && !"admin".equals(username)) {
            throw new BadCredentialsException("用户不存在");
        }
        // 认证密码,暂时不加密
        if ("user".equals(username) && !"123".equals(password) || "admin".equals(username) && !"admin".equals(password)) {
            throw new BadCredentialsException("密码不正确");
        }
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(username,
                authentication.getCredentials(), listUserGrantedAuthorities(username));
        result.setDetails(authentication.getDetails());
        return result;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
    }

    private Set<GrantedAuthority> listUserGrantedAuthorities(String username) {
        Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
        if (CheckUtils.isEmpty(username)) {
            return authorities;
        }
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        if ("admin".equals(username)) {
            authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
        }
        return authorities;
    }
}

MobileCodeAuthenticationProvider

package com.biboheart.demos.security.provider;

import com.biboheart.brick.utils.CheckUtils;
import com.biboheart.demos.security.tokens.MobileCodeAuthenticationToken;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.util.HashSet;
import java.util.Set;

public class MobileCodeAuthenticationProvider implements AuthenticationProvider {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String mobile = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();
        String code = (String) authentication.getCredentials();
        if (CheckUtils.isEmpty(code)) {
            throw new BadCredentialsException("验证码不能为空");
        }
        if (!"13999990000".equals(mobile)) {
            throw new BadCredentialsException("用户不存在");
        }
        // 手机号验证码业务还没有开发,先用4个0验证
        if (!code.equals("0000")) {
            throw new BadCredentialsException("验证码不正确");
        }
        MobileCodeAuthenticationToken result = new MobileCodeAuthenticationToken(mobile,
                listUserGrantedAuthorities(mobile));
        result.setDetails(authentication.getDetails());
        return result;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        return (MobileCodeAuthenticationToken.class.isAssignableFrom(authentication));
    }

    private Set<GrantedAuthority> listUserGrantedAuthorities(String username) {
        Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
        if (CheckUtils.isEmpty(username)) {
            return authorities;
        }
        authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
        return authorities;
    }

}

完成Provider后,要将两个Provider加入配置中,使它们加入工作。修改SecurityConfiguration配置文件。首先实例化这两个Provider,然后将两Bean添加到configure(AuthenticationManagerBuilder auth)

package com.biboheart.demos.security;

import com.biboheart.demos.security.provider.MobileCodeAuthenticationProvider;
import com.biboheart.demos.security.provider.UsernamePasswordAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 创建内存用户
        /*auth.inMemoryAuthentication()
                .withUser("user").password(passwordEncoder.encode("123")).roles("USER")
                .and()
                .withUser("admin").password(passwordEncoder.encode("admin")).roles("USER", "ADMIN");*/
        auth
                .authenticationProvider(usernamePasswordAuthenticationProvider())
                .authenticationProvider(mobileCodeAuthenticationProvider());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    .antMatchers("/", "/home").permitAll() // 这三个目录不做安全控制
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .loginPage("/login")// 自定义的登录页面
                    .permitAll()
                    .and()
                .logout()
                    .logoutSuccessUrl("/");
    }

    @Bean
    public UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider() {
        return new UsernamePasswordAuthenticationProvider();
    }

    @Bean
    public MobileCodeAuthenticationProvider mobileCodeAuthenticationProvider() {
        return new MobileCodeAuthenticationProvider();
    }

    // spring security 必须有一个passwordEncoder
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

增加过滤器

在这个时候,UsernamePasswordAuthenticationProvider已经起作用了。因为Spring Security用默认有一个UsernamePasswordAuthenticationFilter过滤器过滤login,在过滤器中会创建UsernamePasswordAuthenticationToken对象,UsernamePasswordAuthenticationProvider能够得到Token进行处理。虽然MobileCodeAuthenticationProvider已经在认证队列中,但是MobileCodeAuthenticationProvider是不会执行认证工作。MobileCodeAuthenticationToken是自定义的,没有地方生成它的实例,return (MobileCodeAuthenticationToken.class.isAssignableFrom(authentication));执行完成这名后就漂过了。
参考Spring Security UsernamePasswordAuthenticationToken的认证方式,我们也在UsernamePasswordAuthenticationFilter之前加一个过滤器,用户判断MobileCodeAuthenticationToken认证方式。可以用指定的参数,或者指定的URL,这里用的是URL判断,提供“/mobileCodeLogin”为手机号验证码登录的URL。这个Filter参考用户名密码的Filter实现,名称为MobileCodeAuthenticationFilter,从AbstractAuthenticationProcessingFilter继承。接收两个参数分别为“mobile”和“code”。如果比较下,会发现与UsernamePasswordAuthenticationFilter非常像。代码如下:

package com.biboheart.demos.filter;

import com.biboheart.demos.security.tokens.MobileCodeAuthenticationToken;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class MobileCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
    public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
    public static final String SPRING_SECURITY_FORM_CODE_KEY = "code";

    private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
    private String codeParameter = SPRING_SECURITY_FORM_CODE_KEY;
    private boolean postOnly = true;

    public MobileCodeAuthenticationFilter() {
        super(new AntPathRequestMatcher("/mobileCodeLogin", "POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {
        if (postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        
        String mobile = obtainMobile(request);
        String code = obtainCode(request);
        
        if (mobile == null) {
            mobile = "";
        }

        if (code == null) {
            code = "";
        }
        
        mobile = mobile.trim();
        code = code.trim();
        
        AbstractAuthenticationToken authRequest = new MobileCodeAuthenticationToken(mobile, code);
        
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        
        return this.getAuthenticationManager().authenticate(authRequest);
    }
    
    protected String obtainMobile(HttpServletRequest request) {
        return request.getParameter(mobileParameter);
    }
    
    protected String obtainCode(HttpServletRequest request) {
        return request.getParameter(codeParameter);
    }
    
    protected void setDetails(HttpServletRequest request,
            AbstractAuthenticationToken authRequest) {
        authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
    }

}

完成Filter实现后,需要将它加入到Filter序列中。加入方法是在SecurityConfiguration文件中,实例化Filter,然后在configure(HttpSecurity http)配置下加入http.addFilterBefore(mobileCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);即在UsernamePasswordAuthenticationFilter之前加入一个过滤器。记得将“/mobileCodeLogin”添加到允许通得中。修改后的SecurityConfiguration如下:

package com.biboheart.demos.security;

import com.biboheart.demos.filter.MobileCodeAuthenticationFilter;
import com.biboheart.demos.security.provider.MobileCodeAuthenticationProvider;
import com.biboheart.demos.security.provider.UsernamePasswordAuthenticationProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Autowired
    private BCryptPasswordEncoder passwordEncoder;
    @Autowired
    @Qualifier("authenticationManagerBean")
    private AuthenticationManager authenticationManager;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 创建内存用户
        /*auth.inMemoryAuthentication()
                .withUser("user").password(passwordEncoder.encode("123")).roles("USER")
                .and()
                .withUser("admin").password(passwordEncoder.encode("admin")).roles("USER", "ADMIN");*/
        auth
                .authenticationProvider(usernamePasswordAuthenticationProvider())
                .authenticationProvider(mobileCodeAuthenticationProvider());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // @formatter:off
        http
                .authorizeRequests()
                    .antMatchers("/", "/home", "/mobileCodeLogin").permitAll() // 这三个目录不做安全控制
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .loginPage("/login")// 自定义的登录页面
                    .permitAll()
                    .and()
                .logout()
                    .logoutSuccessUrl("/");
        http.addFilterBefore(mobileCodeAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
        // @formatter:on
    }

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public MobileCodeAuthenticationFilter mobileCodeAuthenticationFilter() {
        MobileCodeAuthenticationFilter filter = new MobileCodeAuthenticationFilter();
        filter.setAuthenticationManager(authenticationManager);
        return filter;
    }

    @Bean
    public UsernamePasswordAuthenticationProvider usernamePasswordAuthenticationProvider() {
        return new UsernamePasswordAuthenticationProvider();
    }

    @Bean
    public MobileCodeAuthenticationProvider mobileCodeAuthenticationProvider() {
        return new MobileCodeAuthenticationProvider();
    }

    // spring security 必须有一个passwordEncoder
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

修改界面

在登录界面中加入手机号验证码登录方式,试下效果。手机号和验证码在写在代码中的,分别是13999990000和0000。登录界面修改成:

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Spring Security Example</title>
</head>
<body>
    密码登录:
    <hr/>
    <form th:action="@{/login}" method="post">
        <div>
            <label> 用户名: <input type="text" name="username" />
            </label>
        </div>
        <div>
            <label> 密码: <input type="password" name="password" />
            </label>
        </div>
        <div>
            <input type="submit" value="登录" />
        </div>
    </form>
    <hr/>
    验证码登录:
    <hr/>
    <form th:action="@{/mobileCodeLogin}" method="post">
        <div>
            <label> 手机号: <input type="text" name="mobile" />
            </label>
        </div>
        <div>
            <label> 验证码: <input type="password" name="code" />
            </label>
        </div>
        <div>
            <input type="submit" value="登录" />
        </div>
    </form>
</body>
</html>

完成开发

如果需要更多的认证方式,同手机号验证码。步骤如下:

  1. 创建Token,继承之AbstractAuthenticationToken
  2. 创建Provider,实现AuthenticationProvider
  3. 创建Filter,继承之AbstractAuthenticationProcessingFilter
  4. 在配置类中实例化Filter和Provider
  5. 在Filter中处理请求包装Token
  6. Provider实例加入到auth.authenticationProvider
  7. 使用http.addFilterBefore在UsernamePasswordAuthenticationFilter之前加入Filter
    启动服务,访问界面。使用流程与之前相同。区别是登录界面多了验证码登录表单,输入13999990000,验证码0000后也可以成功登录。
    登录界面

    此时,目录结构如下图。
    目录结构

    源码地址:https://gitee.com/biboheart/bh-springboot-demos.git
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269