SpringBoot 基于SpringSecurity进行验证码登录和JWT验证的整理

在项目开发过程中,由于使用了依赖于SpringSecurity的框架,此框架集成了基于数据库的账号密码登录的功能,而且一直使用很正常,后来由于项目发展和扩充,需要通过手机号验证码进行登录与授权操作,那么在这个时候,突然发现,框架,虽然一直在使用,但是没有仔细学习过SpringSecurity的安全认证框架,对它的验证流程以及配置,并不了解,只能临时查看文档,参考已经实现的账号密码登录功能。进行学习和修改,从学习文档,到完成使用,大概经过了16个小时(2天)的时候,终于将所使用到的基本原理,以及登录进行了实现。下面将主要的实现步骤进行记录,以防忘记,并且做为回忆使用。
Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。


image.png

简单的Security工作流程
原有系统中已经配置完成,而且网上太多讲解如何开启配置的教程,这里只记录自己完成功能所需要的功能

1.增加了三个基础的配置文件,分别为:

Provider Token 以及ServiceImpl他们三个分别继承自:AuthenticationProvider ,AbstractAuthenticationToken和UserDetailsService
如果要自己写生成的用户详情,必须实现UserDetailsService接口,否则只能使用Security自带的,Security自带有,可实现账号和密码登录,但是不能实现自定义登录。

2.具体文件

OpenIdAuthenticationProvider.java


public class OpenIdAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        OpenIdAuthenticationToken authenticationToken = (OpenIdAuthenticationToken) authentication;

        String telephone = (String) authenticationToken.getPrincipal();

        /**
         * 验证码
         */
        // String credentials = (String) authenticationToken.getCredentials();

        UserDetails userDetails = userDetailsService.loadUserByUsername(telephone);

        // 此时鉴权成功后,应当重新 new 一个拥有鉴权的 authenticationResult 返回
        OpenIdAuthenticationToken authenticationResult = new OpenIdAuthenticationToken(userDetails,
                userDetails.getAuthorities());

        authenticationResult.setDetails(authenticationToken.getDetails());

        return authenticationResult;
    }

    @Override
    public boolean supports(Class<?> authentication) {
        // 判断 authentication 是不是 SmsCodeAuthenticationToken 的子类或子接口
        return OpenIdAuthenticationToken.class.isAssignableFrom(authentication);
    }

    public UserDetailsService getUserDetailsService() {
        return userDetailsService;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }
}

OpenIdAuthenticationToken.java


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

    /**
     * 在 UsernamePasswordAuthenticationToken 中该字段代表登录的用户名, 在这里就代表登录的手机号码
     */
    private final Object principal;

    /**
     * 构建一个没有鉴权的 SmsCodeAuthenticationToken
     */
    public OpenIdAuthenticationToken(Object principal) {
        super(null);
        this.principal = principal;
        setAuthenticated(false);
    }

    /**
     * 构建拥有鉴权的 SmsCodeAuthenticationToken
     */
    public OpenIdAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = principal;
        // must use super, as we override
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials() {
        return null;
    }

    @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();
    }

}

自定义此文件,可以重写接收到的数据, private final Object principal; 自定义以及private final Object credentials;
由于这里使用微信OpeId登录,所以只保留了第一个参数,没有使用第二个参数。
UserDetailsByOpenIdServiceImpl.java

@Service("userDetailsByOpenIdServiceImpl")
public class UserDetailsByOpenIdServiceImpl implements UserDetailsService {
    private static final Logger log = LoggerFactory.getLogger(UserDetailsByOpenIdServiceImpl.class);

    @Autowired
    private IBaseConsignorLoginAccountService consignorAccountService;

    @Override
    public UserDetails loadUserByUsername(String openId) throws UsernameNotFoundException {
        ConsignorUser user = consignorAccountService.queryByWeiXinMiniAppOPenId((openId));

        if (StringUtils.isNull(user)) {
            log.info("登录用户:{} 不存在.", openId);
            throw new UsernameNotFoundException("未绑定用户或用户已停用");
        } 
        return createLoginUser(user);
    }

    public UserDetails createLoginUser(ConsignorUser user) {
        return new ConsignorLoginUser(IdUtil.randomUUID(), user.getId(), user);// permissionService.getMenuPermission(user));
    }
}

注意:

@Service("userDetailsByOpenIdServiceImpl")

指定实例化的时候,所要实例化的名称,这里记录名称是为了在配置时使用这个名称,否则会提示已经实例化过UserDetailService。

3.Security配置

要继承WebSecurityConfigurerAdapter下面是几个关键代码

/**
 * spring security配置
 */
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * 
     */
    @Autowired
    @Qualifier("userDetailsByOpenIdServiceImpl")  //指定上面在@Service中定义的名称
    private UserDetailsService userDetailsByOpenIdServiceImpl;

实例化:Provide


        OpenIdAuthenticationProvider openIduthenticationProvider = new OpenIdAuthenticationProvider();
        openIduthenticationProvider.setUserDetailsService(userDetailsByOpenIdServiceImpl);
进行绑定验证
// OPenId验证
        httpSecurity.authenticationProvider(openIduthenticationProvider);

以上配置在configure函数中配置,只要重写此函数即可
主要内容如下

/**
     * anyRequest | 匹配所有请求路径 access | SpringEl表达式结果为true时可以访问 anonymous | 匿名可以访问
     * denyAll | 用户不能访问 fullyAuthenticated | 用户完全认证可以访问(非remember-me下自动登录)
     * hasAnyAuthority | 如果有参数,参数表示权限,则其中任何一个权限可以访问 hasAnyRole |
     * 如果有参数,参数表示角色,则其中任何一个角色可以访问 hasAuthority | 如果有参数,参数表示权限,则其权限可以访问 hasIpAddress
     * | 如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问 hasRole | 如果有参数,参数表示角色,则其角色可以访问 permitAll
     * | 用户可以任意访问 rememberMe | 允许通过remember-me登录的用户访问 authenticated | 用户登录后可访问
     */
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {



        OpenIdAuthenticationProvider openIduthenticationProvider = new OpenIdAuthenticationProvider();
        openIduthenticationProvider.setUserDetailsService(userDetailsByOpenIdServiceImpl);

        httpSecurity
                // CSRF禁用,因为不使用session
                .csrf().disable()
                // 认证失败处理类
                .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
                // 基于token,所以不需要session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()

                // 过滤请求
                .authorizeRequests().antMatchers("/websocket/**").anonymous().antMatchers("/app/overt/**").anonymous()
                .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js").permitAll()
                .antMatchers(securityProperties.getAnonymous()).anonymous()
                .antMatchers(securityProperties.getPermitAll()).permitAll()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated().and().headers().frameOptions().disable();
        httpSecurity.logout().logoutUrl(securityProperties.getLogoutUrl()).logoutSuccessHandler(logoutSuccessHandler);

        // OPenId验证
        httpSecurity.authenticationProvider(openIduthenticationProvider);
        // 添加JWT filter
        httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 添加CORS filter
        httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
        httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);

4.JWT的使用

通过以上登录完成后,即可通过JWT包,返回JWT数据,

5.JWT的验证

JwtAuthenticationTokenFilter 继承自OncePerRequestFilter 实现JWT的认证

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
{
    @Autowired
    private TokenService tokenService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException
    {
        try
        {
            ConsignorLoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication())) {
            tokenService.verifyToken(loginUser);
            UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
            authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        }
        chain.doFilter(request, response);
        }
        catch(RuntimeException ex)
        {
            ExceptionRequest.writeMessage(response, ex.getMessage(), 500);
        }
    }
}

通过Security获取到的信息,直接绑定到UsernamePasswordAuthenticationToken即可完成认证。
至此通过Security和JWT完成登录和认证

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容