总体流程概述:
一、未登录
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());
}
}