总体流程概述:
一、未登录
1、打开登录页,请求被token过滤器类JwtAuthenticationTokenFilter拦截,从请求头中查询token,发现没有,接着调用后台生成验证码接口,生成验证码,将验证码值存入redis,然后,将uuid和验证码图片,响应会前端页面。
2、输入用户名和密码,提交表单登录请求,同样先被token过滤器类JwtAuthenticationTokenFilter拦截,从请求头中查询token,发现没有,接着执行后台的表单请求。根据前台的uuid,
获取redis中缓存的验证码值,如果为空,登录失败,如果验证码值和用户前台输入的码值不一样,登录同样失败。
3、验证码校验通过之后,开始进行用户认证登录,登录成功之后,就可以拿到用户信息
4、生成uuid,将登录的用户信息,缓存到redis中,并设置失效时间为30分钟
5、最后,使用jwt生成token令牌,然后将token令牌返回给前台,登录完成
二、已登录
1、打开某个页面,请求被token过滤器类JwtAuthenticationTokenFilter拦截,从请求头中获取到token。
2、使用jwt对token令牌进行解析,然后从redis中,拿到对应的用户信息,如果redis的失效时间和当前时间相差不足20分钟时,则重新刷新缓存时间为30分钟
3、最后进行用户的认证,认证通过,则可以访问页面
详细流程:
1、打开登录页面,前端发送获取验证码请求接口
(1)首先请求会被自定义的token过滤器拦截,即JwtAuthenticationTokenFilter extends OncePerRequestFilter ,从前端的请求头里面,获取token,判断token是否存在,首次登录token为空
//token过滤器 验证token有效性 JwtAuthenticationTokenFilter extends OncePerRequestFilter{ ....... LoginUser loginUser = tokenService.getLoginUser(request); } //从request中获取token String token = request.getHeader(header);//header为令牌自定义标识:Authorization
//yml文件中配置的token信息
(2)进入后台生成验证码接口,将计算出的验证码值,保存到redis中,有效时间2分钟,步骤如下:
a、生成uuid String uuid = IdUtils.simpleUUID(); b、生成redis的key String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; c、生成验证码图片以及结果 String capText = captchaProducerMath.createText();// 类似于 8*3=?@24 capStr = capText.substring(0, capText.lastIndexOf("@")); code = capText.substring(capText.lastIndexOf("@") + 1); image = captchaProducerMath.createImage(capStr); d、将验证码图片的计算结果,保存到redis中,key为verifyKey,值是code,有效时间2分钟 redisCache.setCacheObject(verifyKey, code, 2, TimeUnit.MINUTES); e、返回验证码图片,以及uuid给前端 FastByteArrayOutputStream os = new FastByteArrayOutputStream(); ImageIO.write(image, "jpg", os); AjaxResult ajax = AjaxResult.success(); ajax.put("uuid", uuid); ajax.put("img", Base64.encode(os.toByteArray())); return ajax;
(3)用户在前端页面输入验证码值,提交表单
(4)请求依然先被token过滤器JwtAuthenticationTokenFilter拦截,此时token依然为空
(5)进入后端登录代码,获取前端传过来的 uuid、username、password、输入的验证码值。
a、首先根据uuid,拼出redis的key,再根据key从redis中取出对应的原始验证码值,然后删掉redis中的key, public String login(String username, String password, String code, String uuid){ String verifyKey = Constants.CAPTCHA_CODE_KEY + uuid; String captcha = redisCache.getCacheObject(verifyKey); redisCache.deleteObject(verifyKey); } b、如果从redis取出的数据是空,那么登录失败,如果从redis取出的值与前端用户输入的验证码值不相等,那么也是登录失败 if (captcha == null) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.expire"))); throw new CaptchaExpireException(); } if (!code.equalsIgnoreCase(captcha)) { AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error"))); throw new CaptchaException(); } c、验证码校验完之后,接下来开始进行用户的登录认证,执行security的用户认证方法 Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password)); 该方法会接着去调用UserDetailsServiceImpl.loadUserByUsername(String username) d、登录成功,获取用户信息 AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success"))); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); e、生成token,返回给前端 public String createToken(LoginUser loginUser) { String token = IdUtils.fastUUID(); loginUser.setToken(token); //设置用户代理信息 setUserAgent(loginUser); //刷新令牌有效期 refreshToken(loginUser); Map<String, Object> claims = new HashMap<>(); claims.put(Constants.LOGIN_USER_KEY, token); return createToken(claims); } ///刷新令牌有效期:设置有效时间是30分钟,并将用户信息缓存到redis中 public void refreshToken(LoginUser loginUser) { loginUser.setLoginTime(System.currentTimeMillis()); loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE); // 根据uuid将loginUser缓存 String userKey = getTokenKey(loginUser.getToken()); //expireTime为30 redisCache.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES); } //从数据声明生成令牌,其中secret为自定义好的令牌秘钥,放在yml文件了,上面已经截图 private String createToken(Map<String, Object> claims) { String token = Jwts.builder() .setClaims(claims) .signWith(SignatureAlgorithm.HS512, secret).compact(); return token; }
2、已经登录过,点击某一个页面时,后台发送的请求流程如下:
(1)首先请求会被自定义的token过滤器拦截,即JwtAuthenticationTokenFilter extends OncePerRequestFilter ,从前端的请求头里面,获取token,因为已经登录,所以此时token是有值的
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private TokenService tokenService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { LoginUser 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); } }
/** * 获取用户身份信息 * * @return 用户信息 */ public LoginUser getLoginUser(HttpServletRequest request) { // 获取请求携带的令牌 String token = getToken(request); if (StringUtils.isNotEmpty(token)) { Claims claims = parseToken(token); // 解析对应的权限以及用户信息 String uuid = (String) claims.get(Constants.LOGIN_USER_KEY); String userKey = getTokenKey(uuid); LoginUser user = redisCache.getCacheObject(userKey); return user; } return null; }
private String getToken(HttpServletRequest request) { String token = request.getHeader(header); if (StringUtils.isNotEmpty(token) && token.startsWith(Constants.TOKEN_PREFIX)) { token = token.replace(Constants.TOKEN_PREFIX, ""); } return token; }
private Claims parseToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); }
private String getTokenKey(String uuid) { return Constants.LOGIN_TOKEN_KEY + uuid; }
总结:
1、前端的每一次请求,后端都会通过token过滤器JwtAuthenticationTokenFilter进行拦截,从请求头里面获取token,然后调用jwt相关api接口,进行解析拿到用户的信息,进行登录认证,认证通过,则放行。
2、登录成功,后台生成token返回给前端,之后前端的每一次请求都会带着这个token
补充一下:
token过滤器需要添加到spring security 配置类里面,才能生效。具体如下:
package com.ruoyi.framework.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.logout.LogoutFilter; import org.springframework.web.filter.CorsFilter; import com.ruoyi.framework.security.filter.JwtAuthenticationTokenFilter; import com.ruoyi.framework.security.handle.AuthenticationEntryPointImpl; import com.ruoyi.framework.security.handle.LogoutSuccessHandlerImpl; /** * spring security配置 * * @author ruoyi */ @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { /** * 自定义用户认证逻辑 */ @Autowired private UserDetailsService userDetailsService; /** * 认证失败处理类 */ @Autowired private AuthenticationEntryPointImpl unauthorizedHandler; /** * 退出处理类 */ @Autowired private LogoutSuccessHandlerImpl logoutSuccessHandler; /** * token认证过滤器 */ @Autowired private JwtAuthenticationTokenFilter authenticationTokenFilter; /** * 跨域过滤器 */ @Autowired private CorsFilter corsFilter; /** * 解决 无法直接注入 AuthenticationManager * * @return * @throws Exception */ @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 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 { httpSecurity // CSRF禁用,因为不使用session .csrf().disable() // 认证失败处理类 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 过滤请求 .authorizeRequests() // 对于登录login 验证码captchaImage 允许匿名访问 .antMatchers("/login", "/captchaImage").anonymous() .antMatchers( HttpMethod.GET, "/*.html", "/**/*.html", "/**/*.css", "/**/*.js" ).permitAll() .antMatchers("/profile/**").anonymous() .antMatchers("/common/download**").anonymous() .antMatchers("/common/download/resource**").anonymous() .antMatchers("/swagger-ui.html").anonymous() .antMatchers("/doc.html").anonymous() .antMatchers("/swagger-resources/**").anonymous() .antMatchers("/webjars/**").anonymous() .antMatchers("/*/api-docs").anonymous() .antMatchers("/druid/**").anonymous() .antMatchers("/hiapi/**").anonymous() .antMatchers("/magic/**").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 添加JWT filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加CORS filter httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class); } /** * 强散列哈希加密实现 */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } /** * 身份认证接口 */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); } }