相关文章推荐
紧张的红金鱼  ·  PHP ...·  5 月前    · 
健身的雪糕  ·  NoClassDefFoundError:o ...·  1 年前    · 

基于Spring Security实现自定义认证-以短信登录为例

《Spring Security实战》、慕课网《Spring Security技术栈开发企业级认证与授权》笔记

实现基于Spring Security的认证有两种方式

  • 增加一个过滤器继承OncePerRequestFilter,将这个Filter放到HttpSecurity的合适的位置。(继承OncePerRequestFilter的目的是确保一次请求只通过一次该过滤器)
  • 基于Spring Security的自定义认证
  • 方法1的那种过滤器的方式大家应该很熟悉,就不展开记录了,下面详细说一下基于Spring Security的自定义认证。

    先以UsernamePassword认证为例,先捋一下认证流程。

    Authentication:Spring Security验证的封装类,包括权限、确定身份正确的凭据、身份详细信息、是否被验证。常见的实现类有 RememberMeAuthenticationToken UsernamePasswordAuthenticationToken

    AuthenticationProvider:Spring Security的一个验证过程,一次完整的认证可以包含多个AuthenticationProvider,一般由ProviderManager管理。Authentication在AuthenticationProvider中流动。用大白话说就是不同的AuthenticationProvider提供不同的token。

    AuthenticationManager:处理Authentication的请求,整个系统只有一个。ProviderManager是AuthenticationManager的实现类

    UsernamePassword认证流程

  • 进入 UsernamePasswordAuthenticationFilter 类: attemptAuthentication() 方法中将前端传过来的 username password 封装到 UsernamePasswordAuthenticationToken 中并标记为未认证。最后调用 this.getAuthenticationManager().authenticate(authRequest) 交给AuthenticationManager处理。
  • ProviderManager根据传入的token类从众多的AuthenticationProvider中找出合适的AuthenticationProvider来处理改认证。
  • 在具体的XXXAuthenticationProvider中认证用户返回带有认证通过和详细信息的token。
  • 所以我们基于Spring Security自定义一个认证要新建一个XXXAuthenticationToken和XXXAuthenticationProvider,最后将自己的逻辑加入到HttpSecurity的过滤器链中。

    下边以短信登录为例实践上边的知识点。

    短信登录逻辑:

  • 前端输入手机号,然后获取短信验证码,最后带着手机号和验证码一起登录
  • 服务端要监听这个登录地址,校验验证码是否正确
  • 正确使用Spring Security自定义认证颁发一个token给前端。
  • 下面贴代码:

    SmsValidateCodeFilter

    短信验证码过滤器,验证短信登录验证码是否正确

    package com.zchi.customizeAuthentication.security.smsCode;
    import lombok.Setter;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.util.AntPathMatcher;
    import org.springframework.web.bind.ServletRequestBindingException;
    import org.springframework.web.bind.ServletRequestUtils;
    import org.springframework.web.context.request.ServletWebRequest;
    import org.springframework.web.filter.OncePerRequestFilter;
    import javax.servlet.FilterChain;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
     * @Description 短信验证码过滤器,验证短信登录验证码是否正确
     * @Author 张弛
     * @Datee 2021/7/4
     * @Version 1.0
    @Setter
    public class SmsValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
        private AntPathMatcher pathMatcher = new AntPathMatcher();
        private AuthenticationFailureHandler authenticationFailureHandler;
        @Override
        protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            boolean action = false;
            String url = "/authentication/smsLogin";
            if (pathMatcher.match(url, httpServletRequest.getRequestURI())) {
                action = true;
            if (action) {
                try {
                    validate(new ServletWebRequest(httpServletRequest));
                } catch (ValidateCodeException e) {
                    authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
                    return;
            filterChain.doFilter(httpServletRequest, httpServletResponse);
        private void validate(ServletWebRequest request) throws ServletRequestBindingException {
            // todo 获取验证码,现在先写死,之后改成从session或者redis中获取
            String codeInSession = "f123";
            String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
            if (StringUtils.isBlank(codeInRequest)) {
                throw new ValidateCodeException("验证码的值不能为空");
            if(codeInSession == null){
                throw new ValidateCodeException("验证码不存在");
            // todo 验证码是否过期
            if(!StringUtils.equals(codeInSession, codeInRequest)) {
                throw new ValidateCodeException("验证码不匹配");
            // todo 使用完这个验证码就删除
        public void setFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
            this.authenticationFailureHandler = authenticationFailureHandler;
    

    SmsCodeAuthenticationToken

    直接照着UsernamePasswordAuthenticationToken写就行

    package com.zchi.customizeAuthentication.security.smsCode;
    import org.springframework.security.authentication.AbstractAuthenticationToken;
    import org.springframework.security.core.GrantedAuthority;
    import javax.security.auth.Subject;
    import java.util.Collection;
     * @Description 直接照着UsernamePasswordAuthenticationToken写就行
     * @Author 张弛
     * @Datee 2021/7/3
     * @Version 1.0
    public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
        private final Object principal;
        public SmsCodeAuthenticationToken(String mobile) {
            super(null);
            this.principal = mobile;
            setAuthenticated(false);
        public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
            super(authorities);
            this.principal = principal;
            setAuthenticated(true);
        @Override
        public Object getCredentials() {
            return this.principal;
        @Override
        public Object getPrincipal() {
            return null;
        @Override
        public boolean implies(Subject subject) {
            return false;
    

    SmsCodeAuthenticationProvider

    Authentication的提供者,根据之前SmsCodeAuthenticationFilter中存入token中的信息获取当前用户信息。声明支持的token类型

    package com.zchi.customizeAuthentication.security.smsCode;
    import lombok.Data;
    import org.springframework.security.authentication.AuthenticationProvider;
    import org.springframework.security.authentication.InternalAuthenticationServiceException;
    import org.springframework.security.core.Authentication;
    import org.springframework.security.core.AuthenticationException;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
     * @Description Authentication的提供者,根据之前SmsCodeAuthenticationFilter中存入token中的信息获取当前用户信息。声明支持的token类型
     * @Author 张弛
     * @Datee 2021/7/3
     * @Version 1.0
     * @see SmsCodeAuthenticationFilter,SmsCodeAuthenticationToken
    @Data
    public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
        private UserDetailsService userDetailsService;
        @Override
        public Authentication authenticate(Authentication authentication) throws AuthenticationException {
            SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
            UserDetails user = userDetailsService.loadUserByUsername((String) token.getPrincipal());
            if(user == null){
                throw new InternalAuthenticationServiceException("无法获取用户信息");
            SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
            // 需要把未认证中的一些信息copy到已认证的token中
            authenticationResult.setDetails(token);
            return authenticationResult;
        // 该provider支持的token
        @Override
        public boolean supports(Class<?> authentication) {
            return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication);
    

    SmsCodeAuthenticationSecurityConfig

    将自己写的这些类配置到过滤器链上

    package com.zchi.customizeAuthentication.security.smsCode;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.DefaultSecurityFilterChain;
    import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
    import org.springframework.stereotype.Component;
     * @Description 将自己写的这些类配置到过滤器链上
     * @Author 张弛
     * @Datee 2021/7/3
     * @Version 1.0
    @Component
    public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
        @Autowired
        private SmsCodeAuthenticationSuccessHandler authenticationSuccessHandler;
        @Autowired
        private SmsCodeAuthenctiationFailureHandler authenticationFailureHandler;
        @Autowired
        private UserDetailsService userDetailsService;
        @Override
        public void configure(HttpSecurity httpSecurity) {
            SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();
            filter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));
            filter.setAuthenticationFailureHandler(authenticationFailureHandler);
            filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
            SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
            smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
            SmsValidateCodeFilter smsValidateCodeFilter = new SmsValidateCodeFilter();
            smsValidateCodeFilter.setAuthenticationFailureHandler(authenticationFailureHandler);
            httpSecurity.addFilterBefore(smsValidateCodeFilter, UsernamePasswordAuthenticationFilter.class);
            httpSecurity.authenticationProvider(smsCodeAuthenticationProvider)
                    .addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
    

    SecurityConfig

    项目的安全配置类

    package com.zchi.customizeAuthentication.security;
    import com.zchi.customizeAuthentication.security.smsCode.SmsCodeAuthenticationSecurityConfig;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.GrantedAuthority;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.web.authentication.AuthenticationFailureHandler;
    import org.springframework.stereotype.Component;
    import java.util.Collection;
     * @Description 项目的安全配置
     * @Author 张弛
     * @Datee 2021/7/3
     * @Version 1.0
    @Component
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        AuthenticationFailureHandler authenticationFailureHandler;
        @Autowired
        private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfigs;
        @Override
        protected void configure(HttpSecurity http) throws Exception {
                    .authorizeRequests()
                    .antMatchers("/authentication/require",
                            "/login",
                            "/code/*",
                            "/error",
                            "/authentication/smsLogin"
                    .permitAll()
                    .anyRequest()
                    .authenticated()
                    .and()
                    .csrf().disable()
                    // 将我们自己的短信登录配置到项目的过滤器链上
                    .apply(smsCodeAuthenticationSecurityConfigs);
        @Bean
        public UserDetailsService userDetailsService() {
            //获取登录用户信息
            return username -> {
                return new UserDetails() {
                    @Override
                    public Collection<? extends GrantedAuthority> getAuthorities() {
                        return null;
                    @Override
                    public String getPassword() {
                        return null;
                    @Override
                    public String getUsername() {
                        return null;
                    @Override
                    public boolean isAccountNonExpired() {
                        return false;
                    @Override
                    public boolean isAccountNonLocked() {
                        return false;
                    @Override
                    public boolean isCredentialsNonExpired() {
                        return false;
                    @Override
                    public boolean isEnabled() {
                        return false;
    

    《Spring Security实战》

    《Spring Security技术栈开发企业级认证与授权》

    代码连接securityDemo: 《基于Spring Security实现自定义认证-以短信登录为例》代码 (gitee.com)