spring security 短信验证 + Jwt (完整版,有源码)

1、spring security的基本工作流程

首先 我们需要了解spring security的基本工作流程

在这里插入图片描述

  • 当登录请求进来时,会在UsernamePasswordAuthenticationFilter 里构建一个没有权限的 Authentication ,

  • 然后把Authentication 交给 AuthenticationManager 进行身份验证管理

  • 而AuthenticationManager 本身不做验证 ,会交给 AuthenticationProvider 进行验证

  • AuthenticationProvider 会调用 UserDetailsService 对用户信息进行校验

    • 我们可以自定义自己的类来 实现UserDetailsService 接口
    • 就可以根据自己的业务需要进行验证用户信息 , 可以从数据库进行用户账号密码校验
    • 也可以 在redis 中用手机号和code 进行校验
  • UserDetailsService 校验成功后 会返回 UserDetails类,里面存放着用户祥细信息

    • 一般我们会重新写一个自定义的类来继承 UserDetails ,方便数据转换。
  • 验证成功后 会重新构造 Authentication 把 UserDetails 传进去,并把认证 改为 true super.setAuthenticated(true)

  • 验证成功后来到 AuthenticationSuccessHandler 验证成功处理器 ,在里面可以返回数据给前端

2、spring security 的 短信登录流程

对基本的流程熟悉后,我们就可以仿照密码登录 来自己定义短信验证方法

在这里插入图片描述

3、代码

根据上图 我们要重写 SmsAuthenticationFilter、SmsAuthenticationProvider、UserDetailsService、UserDetails,来模拟用户密码登录

在写一些配置类来启用我们的短信业务流程 SmsSecurityConfigurerAdapter MySecurityConfig extends WebSecurityConfigurerAdapter

还有自定义成功方法 public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler

自定义 用户认证失败异常 MyAuthenticationEntryPointImpl implements AuthenticationEntryPoint

SmsAuthenticationFilter

public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // 设置拦截/sms/login短信登录接口
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");
    // 认证参数
    private String phoneParameter = "phone";
    private String smsCodeParameter = "code";
    private boolean postOnly = true;

    public SmsAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }
    

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !"POST".equals(request.getMethod())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String phone = this.obtainPhone(request);
            phone = phone != null ? phone : "";
            phone = phone.trim();
            String smsCode = this.obtainSmsCode(request);
            smsCode = smsCode != null ? smsCode : "";
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(phone, smsCode);
            this.setDetails(request, authRequest);
            // 认证信息
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    // request.getParameter 只能获取json格式的数据 
    @Nullable
    protected String obtainSmsCode(HttpServletRequest request) {
        return request.getParameter(this.smsCodeParameter);
    }

    @Nullable
    protected String obtainPhone(HttpServletRequest request) {
        return request.getParameter(this.phoneParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setPhoneParameter(String phoneParameter) {
        Assert.hasText(phoneParameter, "Phone parameter must not be empty or null");
        this.phoneParameter = phoneParameter;
    }

    public void setSmsCodeParameter(String smsCodeParameter) {
        Assert.hasText(smsCodeParameter, "SmsCode parameter must not be empty or null");
        this.smsCodeParameter = smsCodeParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.phoneParameter;
    }

    public final String getPasswordParameter() {
        return this.smsCodeParameter;
    }
}

SmsAuthenticationProvider

验证手机号和短信是否匹配 核心业务

@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {

    private static final String REDIS_LONGIN_PRE = "login:";


    private SmsDetailsServiceImpl smsUserDetailsService;

    private StringRedisTemplate stringRedisTemplate;


    public SmsAuthenticationProvider (SmsDetailsServiceImpl userDetailsServiceImpl,StringRedisTemplate stringRedisTemplate) {
        this.smsUserDetailsService = userDetailsServiceImpl;
        this.stringRedisTemplate = stringRedisTemplate;
    }


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        Object principal = authentication.getPrincipal();// 获取凭证也就是用户的手机号
        String phone = "";
        if (principal instanceof String) {
            phone = (String) principal;
        }
        String inputCode = (String) authentication.getCredentials(); // 获取输入的验证码


        // 1. 检验Redis手机号的验证码
        String redisCode =stringRedisTemplate.opsForValue().get(REDIS_LONGIN_PRE+phone);
        if (StringUtils.isEmpty(redisCode)) {
            throw new BadCredentialsException("验证码已经过期或尚未发送,请重新发送验证码");
        }
        if (!inputCode.equals(redisCode)) {
            throw new BadCredentialsException("输入的验证码不正确,请重新输入");
        }
        // 2. 短信验证成功后要删除redis中的验证码
        stringRedisTemplate.delete(REDIS_LONGIN_PRE+phone);

        // 3. 根据手机号查询用户信息
        LoginUser userDetails = (LoginUser) smsUserDetailsService.loadUserByUsername(phone);
        if (userDetails == null) {
            throw new InternalAuthenticationServiceException("phone用户不存在,请注册");
        }
        // 4. 重新创建已认证对象,
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails,inputCode, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

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

}

UserDetailsService

查询用户信息

@Service
public class SmsDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {

        System.out.println("正在使用SmsDetailsServiceImpl。。。。。");

        // 查询用户信息
        QueryWrapper<UserEntity> wrapper = new QueryWrapper<>();
        System.out.println("手机号为:" + phone);
        wrapper.eq("mobile",phone);

        UserEntity userEntity = userMapper.selectOne(wrapper);

        if(Objects.isNull(userEntity)){
            throw  new RuntimeException("用户不存在");
        }

        //TODO 查询对应权限信息

        LoginUser user = new LoginUser(userEntity, Arrays.asList("test","admin"));
        return user;
    }
}

SmsCodeAuthenticationToken

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;


    // 手机号
    private final Object principal;
    // 验证码
    private Object credentials;

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


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


    public Object getCredentials() {
        return this.credentials;
    }

    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;
    }
}

UserDetails

自定义UserDetails类 方便数据获取

@NoArgsConstructor
@Data
public class LoginUser implements UserDetails {
    private UserEntity userEntity;

    public LoginUser(UserEntity userEntity) {
        this.userEntity = userEntity;
    }

    private List<String> permissions;

    @JSONField(serialize = false)
    List<SimpleGrantedAuthority> authorities;

    public LoginUser(UserEntity userEntity, List<String> permissions) {
        this.userEntity = userEntity;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities!=null)
            return authorities;
        authorities = permissions.stream().map(item -> {
            return new SimpleGrantedAuthority(item);
        }).collect(Collectors.toList());
        return authorities;
    }

    @Override
    public String getPassword() {
        return userEntity.getPassword();
    }

    @Override
    public String getUsername() {
        return userEntity.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

SmsSecurityConfigurerAdapter

单独配置短信验证

@Component
public class SmsSecurityConfigurerAdapter extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {

    @Autowired
    private SmsDetailsServiceImpl userDetailsService;

    @Autowired
    private StringRedisTemplate redisTemplate;

    @Override
    public void configure(HttpSecurity http) throws Exception {

        //自定义SmsCodeAuthenticationFilter过滤器
        SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
        smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsAuthenticationFilter.setAuthenticationSuccessHandler(new MyAuthenticationSuccessHandler());

        //设置自定义SmsCodeAuthenticationProvider的认证器userDetailsService
        SmsAuthenticationProvider smsCodeAuthenticationProvider = new SmsAuthenticationProvider(userDetailsService,redisTemplate);

        //在UsernamePasswordAuthenticationFilter过滤前执行
        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

MySecurityConfig

把 短信验证配置添加到总配置

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    MyAuthenticationEntryPointImpl myAuthenticationEntryPoint;

    @Autowired
    MyAccessDeniedHandlerImpl myAccessDeniedHandler;

    @Autowired
    SmsSecurityConfigurerAdapter smsSecurityConfigurerAdapter;

    @Autowired
    UsernamePassSecurityConfAdapter usernamePassSecurityConfAdapter;

    // 创建盐值加密
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .csrf().disable()
                // 不通过session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 对于登录接口 运行匿名访问
                .authorizeRequests()
                .antMatchers("/login","/sms/login","/sms/login/test","/sms/sendcode").anonymous()
                .antMatchers("/").hasAnyAuthority("admin")
                // 除上面外的所有请求全部要鉴权认证
                .anyRequest().authenticated()
               // .and().apply(usernamePassSecurityConfAdapter)
                .and().apply(smsSecurityConfigurerAdapter);

        // 添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        // 配置异常处理器
        http.exceptionHandling()
                // 配置认证失败处理器
                .authenticationEntryPoint(myAuthenticationEntryPoint);
//                // 配置权限处理器
//                .accessDeniedHandler(myAccessDeniedHandler);
        // 允许跨域
        http.cors();
    }
}

MyAuthenticationSuccessHandler

自定义认证成功方法 主要用来生成token 后在返回给前端

@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    static StringRedisTemplate  stringRedisTemplate;

    private static final String REDIS_LONGIN_PRE = "login:";
    private static final String REDIS_LONGIN_TOKEN = "login:token:";


    // 解决 @Component 下 @Autowired 注入为null的情况
    @Autowired
    public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
        MyAuthenticationSuccessHandler.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//        logger.info("登录成功");

        LoginUser principal = (LoginUser) authentication.getPrincipal();

        // 认证通过了,使用userid生成一个jwt jwt 返回给前端
        String token = new JWTEasyUtil().createToken(principal.getUserEntity().getId());

        // 把token存入redis 并设过期时间
        this.stringRedisTemplate.opsForValue().set(REDIS_LONGIN_TOKEN+principal.getUserEntity().getId(),token,1, TimeUnit.HOURS);

        // 把用户的完整信息存入redis
        this.stringRedisTemplate.opsForValue().set(REDIS_LONGIN_PRE+principal.getUserEntity().getId(), JSONObject.toJSONString(principal));

        // 返回token给前端     前端拿到token 每次请求都要在head里携带token
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(token));
    }
}

MyAuthenticationEntryPointImpl

自定义 用户认证失败异常

// 自定义 用户认证失败异常
@Component
public class MyAuthenticationEntryPointImpl implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("text/html;charset=UTF-8");

        response.getWriter().println("<h1>"+authException.getMessage()+"</h1>");
    }
}

JwtAuthenticationTokenFilter

此过滤器最先执行,用来校验 前端发送请求的token

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    StringRedisTemplate redisTemplate;

    private static final String REDIS_LONGIN_PRE = "login:";
    private static final String REDIS_LONGIN_TOKEN = "login:token:";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        // 获取token
        String token = request.getHeader("token");
        if(StringUtils.isEmpty(token)){
            filterChain.doFilter(request, response);
            return;
        }
        String userId = null;
        // 解析token
        try {
            Claims parseToken = new JWTEasyUtil().parseToken(token);
            userId = parseToken.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
        }
        // 首先判断token值过期没
        String redisToken = redisTemplate.opsForValue().get(REDIS_LONGIN_TOKEN + userId);
        if(StringUtils.isEmpty(redisToken) || (!redisToken.equals(token))){
            throw new AccountExpiredException("token过期,请重新登录");
        }

        // TODO 从redis中获取用户信息
        String userJson = redisTemplate.opsForValue().get(REDIS_LONGIN_PRE + userId);
        // 判断用户是否已经注销了
        if(Objects.isNull(userJson)){
            throw new AccountExpiredException("请重新登录");
        }
        LoginUser loginUser = JSONObject.parseObject(userJson, LoginUser.class);
        // 存入SecurityContextHolder
        // 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);

        filterChain.doFilter(request, response);
    }
}

controller()

@RestController
public class LoginController {

    @Autowired
    LoginService loginService;
    
    @GetMapping("/sms/sendcode")
    public BaseResult sendCode(@RequestParam("phone") String phone) {
        return loginService.sendCode(phone);
    }

    @GetMapping("/loginout")
    public BaseResult loginOut() {
        return loginService.logout();
    }
}
public interface LoginService extends IService<UserEntity> {

    BaseResult logout();

    BaseResult sendCode(String phone);
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 查询用户信息
        QueryWrapper<UserEntity> wrapper = new QueryWrapper<>();
        System.out.println("用户名:"+username);
        wrapper.eq("username",username);

        UserEntity userEntity = userMapper.selectOne(wrapper);

        if(Objects.isNull(userEntity)){
            throw  new RuntimeException("用户不存在");
        }

        //TODO 查询对应权限信息
        LoginUser user = new LoginUser(userEntity,Arrays.asList("test","admin"));
        return user;
    }
}

4、源码分析

  1. 验证登录请求一进来,首先进入这个。 我们要 创建SmsAuthenticationFilter,用来根据手机号来查找用户信息, 此类 仿写UsernamePasswordAuthenticationFilter
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // 设置拦截/sms/login短信登录接口(短信登录接口)
    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login", "POST");
    // 认证  请求参数
    private String phoneParameter = "phone";
    private String smsCodeParameter = "code";
    private boolean postOnly = true;

    public SmsAuthenticationFilter() {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }
    

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !"POST".equals(request.getMethod())) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String phone = this.obtainPhone(request);
            phone = phone != null ? phone : "";
            phone = phone.trim();
            String smsCode = this.obtainSmsCode(request);
            smsCode = smsCode != null ? smsCode : "";
            SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(phone, smsCode);
            this.setDetails(request, authRequest);
            // 认证信息
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

    @Nullable
    protected String obtainSmsCode(HttpServletRequest request) {
        return request.getParameter(this.smsCodeParameter);
    }

    @Nullable
    protected String obtainPhone(HttpServletRequest request) {
        return request.getParameter(this.phoneParameter);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
    }

    public void setPhoneParameter(String phoneParameter) {
        Assert.hasText(phoneParameter, "Phone parameter must not be empty or null");
        this.phoneParameter = phoneParameter;
    }

    public void setSmsCodeParameter(String smsCodeParameter) {
        Assert.hasText(smsCodeParameter, "SmsCode parameter must not be empty or null");
        this.smsCodeParameter = smsCodeParameter;
    }

    public void setPostOnly(boolean postOnly) {
        this.postOnly = postOnly;
    }

    public final String getUsernameParameter() {
        return this.phoneParameter;
    }

    public final String getPasswordParameter() {
        return this.smsCodeParameter;
    }
}
  1. 根据SmsAuthenticationFilter里的return this.getAuthenticationManager().authenticate(authRequest);来到了ProviderManager执行authenticate(Authentication authentication)方法。
public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		AuthenticationException parentException = null;
		Authentication result = null;
		Authentication parentResult = null;
		boolean debug = logger.isDebugEnabled();
		
    	//   遍历验证管理 
    	//  此时的toTest 为 SmsCodeAuthenticationToken
    	//  需要provider遍历到支持SmsCodeAuthenticationToken 的验证方法
    	//  此时血药provider == SmsAuthenticationProvidery 至于为什么请往下看
 		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				result = provider.authenticate(authentication);

  1. 当 getProviders()遍历到 支持SmsCodeAuthenticationToken的 AuthenticationProvider 后,会执行上面Authentication authenticate(Authentication authentication)方法里的 result = provider.authenticate(authentication);

​ 也就来到了 SmsAuthenticationProvider里,执行里面的 public Authentication authenticate(Authentication authentication) throws AuthenticationException {方法 ,在方法里进行短信验证码校验。

@Component
public class SmsAuthenticationProvider implements AuthenticationProvider {

    private static final String REDIS_LONGIN_PRE = "login:";


    private SmsDetailsServiceImpl smsUserDetailsService;

    private StringRedisTemplate stringRedisTemplate;


    public SmsAuthenticationProvider (SmsDetailsServiceImpl userDetailsServiceImpl,StringRedisTemplate stringRedisTemplate) {
        this.smsUserDetailsService = userDetailsServiceImpl;
        this.stringRedisTemplate = stringRedisTemplate;
    }


    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication;
        Object principal = authentication.getPrincipal();// 获取凭证也就是用户的手机号
        String phone = "";
        if (principal instanceof String) {
            phone = (String) principal;
        }
        String inputCode = (String) authentication.getCredentials(); // 获取输入的验证码


        // 1. 检验Redis手机号的验证码
        String redisCode =stringRedisTemplate.opsForValue().get(REDIS_LONGIN_PRE+phone);
        if (StringUtils.isEmpty(redisCode)) {
            throw new BadCredentialsException("验证码已经过期或尚未发送,请重新发送验证码");
        }
        if (!inputCode.equals(redisCode)) {
            throw new BadCredentialsException("输入的验证码不正确,请重新输入");
        }
        // 2. 短信验证成功后要删除redis中的验证码
        stringRedisTemplate.delete(REDIS_LONGIN_PRE+phone);

        // 3. 根据手机号查询用户信息
        LoginUser userDetails = (LoginUser) smsUserDetailsService.loadUserByUsername(phone);
        if (userDetails == null) {
            throw new InternalAuthenticationServiceException("phone用户不存在,请注册");
        }
        // 4. 重新创建已认证对象,
        SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails,inputCode, userDetails.getAuthorities());
        authenticationResult.setDetails(authenticationToken.getDetails());
        return authenticationResult;
    }

    // 当类型为SmsCodeAuthenticationToken的认证实体进入时才走此Provider
    // 正是这个设置  在ProviderManager寻找处理认证时才能找个这个类
    @Override
    public boolean supports(Class<?> aClass) {
        return SmsCodeAuthenticationToken.class.isAssignableFrom(aClass);
    }

}
  1. 经过 SmsAuthenticationProvider里的Authentication authenticate(Authentication authentication) 方法后,会返回到 ProviderManager里继续执行sessionStrategy.onAuthentication(authResult, request, response)方法。

    然后来到AbstractAuthenticationProcessingFilter类里,执行successHandler.onAuthenticationSuccess(request, response, authResult)

    此时就来到了自定义认证成功方法MyAuthenticationSuccessHandler里,执行里面的onAuthenticationSuccess方法。

@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    static StringRedisTemplate  stringRedisTemplate;

    private static final String REDIS_LONGIN_PRE = "login:";
    private static final String REDIS_LONGIN_TOKEN = "login:token:";


    // 解决 @Component 下 @Autowired 注入为null的情况
    @Autowired
    public void setStringRedisTemplate(StringRedisTemplate stringRedisTemplate) {
        MyAuthenticationSuccessHandler.stringRedisTemplate = stringRedisTemplate;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//        logger.info("登录成功");

        LoginUser principal = (LoginUser) authentication.getPrincipal();

        // 认证通过了,使用userid生成一个jwt jwt 返回给前端
        String token = new JWTEasyUtil().createToken(principal.getUserEntity().getId());

        // 把token存入redis 并设过期时间
        this.stringRedisTemplate.opsForValue().set(REDIS_LONGIN_TOKEN+principal.getUserEntity().getId(),token,1, TimeUnit.HOURS);

        // 把用户的完整信息存入redis
        this.stringRedisTemplate.opsForValue().set(REDIS_LONGIN_PRE+principal.getUserEntity().getId(), JSONObject.toJSONString(principal));

        // 返回token给前端     前端拿到token 每次请求都要在head里携带token
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(token));
    }
}

5、测试

发送验证码

在这里插入图片描述

验证码登录

在这里插入图片描述

6、项目源码

地址 https://github.com/Anan-X/spring_security_demo/tree/master

Spring Cloud + JWT + MybatisPlus 的组合为构建微服务架构的后端系统提供了一套完整的解决方案。在这种架构下,Token登录是常见的用户认证方式,它通过生成Token作为身份凭证,避免了在服务间传递用户信息,增强了安全性。以下是一个简化的教程,用于说明如何使用这些技术实现Token登录: 1. **项目依赖和配置**: 首先,你需要在项目的`pom.xml`文件中添加Spring Cloud、JWT、MybatisPlus等相关依赖。这里仅列举关键依赖,具体版本根据实际需求选择: ```xml <!-- Spring Boot Starter --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- Spring Cloud Starter --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <!-- Mybatis Plus --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.x.x</version> </dependency> <!-- JWT --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> ``` 2. **配置JWT**: 在`application.yml`或`application.properties`中配置JWT的密钥和Token过期时间: ```yaml jwt: secret: your-secret-key expiration: 3600000 # Token过期时间,单位毫秒 ``` 3. **创建Token生成和验证工具类**: 使用JWT提供的工具类来生成和验证Token。通常会创建一个`JwtUtil`类来完成这个任务: ```java @Component public class JwtUtil { // 获取Token的工具方法 public static String generateToken(String username) { // 使用HS512算法和密钥生成Token return Jwts.builder() .setSubject(username) .setExpiration(new Date(System.currentTimeMillis() + expiration)) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } // 验证Token的工具方法 public static Claims getClaimsFromToken(String token) { try { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { return null; } } } ``` 4. **创建用户认证接口**: 实现一个基于Token的用户认证接口,该接口接收用户名和密码,验证通过后返回Token: ```java @RestController public class AuthController { @PostMapping("/login") public ResponseEntity<?> createAuthenticationToken(@RequestBody LoginUserDto loginUserDto) { // 这里简化了用户验证和密码加密的过程,实际开发中应使用安全的密码验证方式 if (userExistsAndValid(loginUserDto.getUsername(), loginUserDto.getPassword())) { String token = JwtUtil.generateToken(loginUserDto.getUsername()); return ResponseEntity.ok(new JwtResponse(token)); } else { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid credentials"); } } private boolean userExistsAndValid(String username, String password) { // 检查用户是否存在和密码是否正确 return true; } } ``` 5. **创建Token验证过滤器**: 创建一个过滤器,用于在请求到达Controller前拦截并验证Token的有效性: ```java @Component public class JwtTokenFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { // 从请求头中获取Token String token = request.getHeader("Authorization"); if (token != null && !token.isEmpty()) { try { // 验证Token Claims claims = JwtUtil.getClaimsFromToken(token.replace("Bearer ", "")); // 如果验证成功,则将用户名设置到请求中供后续使用 request.setAttribute("username", claims.getSubject()); filterChain.doFilter(request, response); } catch (Exception e) { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid Token"); } } else { response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Token is missing"); } } } ``` 6. **配置Spring Security**: 配置Spring Security来使用JWT过滤器,并设置认证规则: ```java @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtTokenFilter jwtTokenFilter; @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/login").permitAll() .anyRequest().authenticated(); // 添加JWT过滤器 http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class); } } ``` 以上是基于Spring Cloud + JWT + MybatisPlus 实现Token登录的一个大致流程。在实际开发中,你需要根据具体业务逻辑完善用户服务、密码验证和用户管理等环节。此外,源码通常涉及到整个项目的结构和业务逻辑,这里无法提供完整的源码,但上述步骤可作为实现Token登录的核心指导。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值