『SpringSecurity』(六)自定义多种登陆方式

上一节实现了,用户名密码+验证码的 json 格式登陆。这一节,需求继续升级了。

需求描述

要求此接口可实现:

  1. 用户名密码+验证码登陆
  2. 手机号+验证码登陆
  3. 第三方Id登陆

解决方案

这一节,我们就要使用,上一章提到的自定义身份验证令牌类了。

在认证入口类(MyAuthenticationProcessingFilter)解析出登陆参数(LoginData),然后包装为认证令牌类(MyAuthenticationToken),继续往下传递。到达认证功能提供类(MyAuthenticationTokenProvider)中,进行登陆方式判定,然后分别调用不同的方法进行登陆认证。

并且,我们需要扩展登陆参数类。上一节,我们只有三个参数:usernamepasswordcommonLoginVerifyCode

根据需求,我们需要扩充为:

@Data
public class LoginData {
    /** 登陆方式 */
    private String loginType;
    /** 用户名 */
    private String username;
    /** 密码 */
    private String password;
    /** 普通登陆验证码 */
    private String commonLoginVerifyCode;
    /** 手机号 */
    private String phone;
    /** 手机验证码 */
    private String phoneVerifyCode;
    /** 第三方平台类型 */
    private String thirdPlatformType;
    /** 第三方平台id */
    private String thirdPlatformId;
}
复制代码

实现

无关逻辑均做了简化处理。

  1. 自定义认证类

    模仿 UsernamePasswordAuthenticationToken编写。

    @Getter
    @Setter
    @ToString
    public class MyAuthenticationToken extends AbstractAuthenticationToken {
     
        private final Object principal;
        private Object credentials;
        private LoginData loginData;
     
        public MyAuthenticationToken(Object principal, Object credentials) {
            super(null);
            this.principal = principal;
            this.credentials = credentials;
            setAuthenticated(false);
        }
     
        public MyAuthenticationToken(Object principal, Object credentials,Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            this.credentials = credentials;
            // must use super, as we override
            super.setAuthenticated(true);
        }
     
        @Override
        public Object getCredentials() {
            return this.credentials;
        }
     
        @Override
        public Object getPrincipal() {
            return this.principal;
        }
     
        @Override
        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;
        }
    复制代码
  2. 自定义 Provider

    模仿 DaoAuthenticationProvider 编写。

    @Component
    @Slf4j
    public class MyAuthenticationTokenProvider extends AbstractUserDetailsAuthenticationProvider {
        @Autowired
        MyUserDetailServiceImpl myUserDetailService;
     
        @Override
        protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {}
     
        // 认证逻辑
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            MyAuthenticationToken myAuthenticationToken = (MyAuthenticationToken) authentication;
            log.info("{}",myAuthenticationToken.toString());
            LoginData loginData = myAuthenticationToken.getLoginData();
            if (loginData == null) {
                throw new AuthenticationServiceException("未获取到登陆参数");
            }
            String loginType = loginData.getLoginType();
            if (loginType == null || loginType.equals("")) {
                throw new AuthenticationServiceException("登陆方式不可为空");
            }
            UserDetails userDetails;
            if (LoginType.USERNAME_CODE.equals(loginType)) {
                // 用户名密码登陆
                log.info("尝试以 {} 方式登陆",LoginType.USERNAME_CODE);
            this.checkUsernameCode(loginData.getUsername(),loginData.getCommonLoginVerifyCode());
                userDetails = myUserDetailService.loadUserByUsername(loginData.getUsername());
            }else if (LoginType.PHONE_CODE.equals(loginType)) {
                // 手机号验证码登陆
                log.info("尝试以 {} 方式登陆",LoginType.PHONE_CODE);
                this.checkPhoneCode(loginData.getPhone(),loginData.getPhoneVerifyCode());
                userDetails = myUserDetailService.loadUserByPhone(loginData.getPhone());
            }else if (LoginType.THIRD_PLATFORM.equals(loginType)) {
                // 三方平台登陆
                log.info("尝试以 {} 方式登陆",LoginType.THIRD_PLATFORM);
                userDetails = myUserDetailService.loadByThirdPlatformId(loginData.getThirdPlatformType(),loginData.getThirdPlatformId());
            }else {
                throw new AuthenticationServiceException("不支持的登陆方式");
            }
            // 认证成功
            return this.createSuccessAuthentication(userDetails,myAuthenticationToken,userDetails);
        }
     
        @Override
        protected UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
            return null;
        }
     
       // 此 provider 支持什么类型的令牌类
        @Override
        public boolean supports(Class<?> authentication) {
            return (MyAuthenticationToken.class.isAssignableFrom(authentication));
        }
     
        public void checkPhoneCode(String phone,String code) {
            // todo 校验手机验证码
            if ("111111".equals(code)) {
            }else {
                throw new AuthenticationServiceException("手机验证码错误");
            }
        }
     
        public void checkUsernameCode(String codeId,String code) {
            // todo 校验用户名密码登陆
            if ("222222".equals(code)) {
            }else {
                throw new AuthenticationServiceException("验证码错误");
            }
        }
    }
    复制代码
  3. 修改 Security 配置

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
     
        @Autowired
        MyUserDetailServiceImpl userDetailService;
        @Autowired
        BCryptPasswordEncoder passwordEncoder;
        @Autowired
        MyAuthenticationTokenProvider myAuthenticationTokenProvider;
     
        // 创建自己的AuthenticationManager
        @Override
        @Bean
        protected AuthenticationManager authenticationManager() throws Exception {
            ProviderManager manager = new ProviderManager(Collections.singletonList(myAuthenticationTokenProvider));
            return manager;
        }
     
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailService);
        }
     
        @Bean
        MyAuthenticationProcessingFilter myAuthenticationProcessingFilter() throws Exception {
     
            MyAuthenticationProcessingFilter filter = new MyAuthenticationProcessingFilter();
     
            filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
                response.setContentType("application/json;charset=utf-8");
                PrintWriter out = response.getWriter();
                MyUserDetails userDetails = (MyUserDetails) authentication.getPrincipal();
                userDetails.setPassword(null);
                HashMap<String, Object> result = new HashMap<>();
                result.put("code","000000");
                result.put("msg","登陆成功");
                result.put("data",userDetails);
                String s = new ObjectMapper().writeValueAsString(result);
                out.write(s);
                out.flush();
                out.close();
            });
     
            filter.setAuthenticationFailureHandler((request, response, exception) -> {
                response.setContentType("application/json;charset=utf-8");
                PrintWriter out = response.getWriter();
                HashMap<String, Object> result = new HashMap<>();
                result.put("code","111111");
                if (exception instanceof LockedException) {
                    result.put("msg","账户被锁定!");
                } else if (exception instanceof DisabledException) {
                    result.put("msg","账户被禁用,请联系管理员!");
                } else if (exception instanceof BadCredentialsException) {
                    result.put("msg","用户名或者密码输入错误,请重新输入!");
                } else if (exception instanceof AuthenticationServiceException) {
                    result.put("msg",exception.getMessage());
                }
                out.write(new ObjectMapper().writeValueAsString(result));
                out.flush();
                out.close();
            });
     
            // 把自己的authenticationManager 设置到环境中
            filter.setAuthenticationManager(authenticationManager());
            filter.setFilterProcessesUrl("/toLogin");
            return filter;
        }
     
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .anyRequest()
                    .authenticated()
                    .and()
                    .csrf()
                    .disable();
            // 替换认证拦截器(认证入口)
            http.addFilterAt(myAuthenticationProcessingFilter(), UsernamePasswordAuthenticationFilter.class);
        }
    }
    复制代码

登陆

  1. 不配置登陆方式

    request

    {
        "phone":"987654321",
        "phoneVerifyCode":"111111"
    }
    复制代码

    resp

    {
        "msg": "登陆方式不可为空",
        "code": "111111"
    }
    复制代码
  2. 手机号验证码登陆

    request

    {
        "loginType":"phoneCode",
        "phone":"987654321",
        "phoneVerifyCode":"111111"
    }
    复制代码

    resp

    {
        "msg": "登陆成功",
        "code": "000000",
        "data": {
            "id": 2,
            "phone": null,
            "username": "李四",
            "password": null,
            "accountStatus": 1,
            "roleList": [
                {
                    "id": 1,
                    "name": "admin",
                    "desc": null
                },
                {
                    "id": 2,
                    "name": "user",
                    "desc": null
                }
            ],
            "enabled": true,
            "authorities": [
                {
                    "authority": "admin"
                },
                {
                    "authority": "user"
                }
            ],
            "accountNonExpired": true,
            "credentialsNonExpired": true,
            "accountNonLocked": true
        }
    }
    复制代码
  3. 用户名密码登陆

    request

    {    "loginType":"usernameCode",    "username":"张三",    "password":"123456",    "commonLoginVerifyCode":"222222"}
    复制代码

    resp

    {    "msg": "登陆成功",    "code": "000000",    "data": {        "id": 1,        "phone": null,        "username": "张三",        "password": null,        "accountStatus": 1,        "roleList": [            {                "id": 1,                "name": "admin",                "desc": null            },            {                "id": 2,                "name": "user",                "desc": null            }        ],        "enabled": true,        "authorities": [            {                "authority": "admin"            },            {                "authority": "user"            }        ],        "accountNonExpired": true,        "credentialsNonExpired": true,        "accountNonLocked": true    }}
    复制代码
  4. 第三方openId登陆

    request

    {    "loginType":"thirdPlatform",    "thirdPlatformType":"WeChat",    "thirdPlatformId":"qwqwqqwqwqw"}
    复制代码

    resp

    {    "msg": "登陆成功",    "code": "000000",    "data": {        "id": 1,        "phone": null,        "username": "张三",        "password": null,        "accountStatus": 1,        "roleList": [            {                "id": 1,                "name": "admin",                "desc": null            },            {                "id": 2,                "name": "user",                "desc": null            }        ],        "enabled": true,        "authorities": [            {                "authority": "admin"            },            {                "authority": "user"            }        ],        "accountNonExpired": true,        "credentialsNonExpired": true,        "accountNonLocked": true    }}
    复制代码

这里都是比较简单的demo,实际运用过程中,还需要优化。如,针对多种登陆方式,我们可以提供多种 provider 来进行处理。