若依登录认证
若依登录流程后端的认证部分 SpringSecurity
若依登录认证(后端)
0. SpringSecurity知识点
若项目中加入了SpringSecurity的依赖,会默认对项目的方法都提供保护。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
启动程序的时候,会自动弹出登录认证页面
这个过程的大致流程如图所示,弹窗认证功能是由UsernamePasswordAuthenticationFilter过滤器提供的
SpringSecurity有很多过滤器,但是核心的过滤器主要有三个:
- UsernamePasswordAuthenticationFilter:用户名密码认证过滤器。
负责处理在登陆页面填写了用户名密码后的登陆请求。
- ExceptionTranslationFilter:异常转换过滤器。
处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
- FilterSecurityInterceptor:负责权限校验的过滤器。
SpringSecurity的基础认证流程
这是默认的认证流程,这个过程中UsernamePasswordAuthenticationFilter会调用上述的认证界面对用户进行用户名和密码的认证,并且InMemoryUserDetailsManager是在内存中进行相关操作的,这两点和实际开发不太符合。
我们可以自定义login接口,在这个方法中手动调用AuthenticationManager的authenticate方法。由于默认的loadUserByUsername是对内存操作的,我们可以改写该方法,查询数据库中用户名及密码等相关信息
自定义的认证流程
1. 认证
登录校验
自定义登录接口 login,在这个方法中手动调用AuthenticationManager的authenticate方法对用户进行认证。
/**
自定义登录接口 login
在这个方法中手动调用AuthenticationManager的authenticate方法对用户进行认证
*/
loginService.login(loginBody.getUsername(), loginBody.getPassword(), loginBody.getCode(),loginBody.getUuid());
若验证码校验通过并且用户名和密码前置校验通过后,会调用AuthenticationManager的authenticate方法进行用户认证
@Resource
private AuthenticationManager authenticationManager;
// TODO 验证码校验 登录前置校验
...
try{
/*
封装Authentication对象
把用户的用户名和密码传入,方便后续authenticationManager(该对象的authenticate传入的对象是Authentication authentication)就行认证
*/
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
//存储Authentication对象 ThreadLocal 存数据
AuthenticationContextHolder.setContext(authenticationToken);
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername(自定义的loadUserByUsername方法)
authentication = authenticationManager.authenticate(authenticationToken);
}
catch()
...
//TODO 异常捕捉
finally
{
/**
* ThreadLocal确保了每个线程使用一个认证对象,不用每次使用都传递 Authentication 认证对象了,但是在本次线程完必须清除数据,
* (因为spring使用的线程池,请求完不会销毁线程,回到线程池由下一个请求继续使用,防止携带'前世'数据,本次请求完需销毁)
*/
//ThreadLocal 销毁
AuthenticationContextHolder.clearContext();
}
数据库校验
未重写的loadUserByUsername是对内存进行处理,重写后的loadUserByUsername可以根据自身的业务逻辑进行编写,先获取数据库中该用户名的相关信息,再对密码的有效性校验,最后返回UserDetails对象
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询 该用户的用户信息、部门信息、角色信息
SysUser user = userService.selectUserByUserName(username);
//抛出的ServiceException由GlobalExceptionHandler中的handleServiceException捕捉
if (StringUtils.isNull(user))
{
log.info("登录用户:{} 不存在.", username);
throw new ServiceException(MessageUtils.message("user.not.exists"));
}
else if (UserStatus.DELETED.getCode().equals(user.getDelFlag()))
{
log.info("登录用户:{} 已被删除.", username);
throw new ServiceException(MessageUtils.message("user.password.delete"));
}
else if (UserStatus.DISABLE.getCode().equals(user.getStatus()))
{
log.info("登录用户:{} 已被停用.", username);
throw new ServiceException(MessageUtils.message("user.blocked"));
}
//校验用户密码的正确性
passwordService.validate(user);
//返回UserDetails对象
return createLoginUser(user);
}
public void validate(SysUser user)
{
//获取用户登入时 填写的用户名和密码
Authentication usernamePasswordAuthenticationToken = AuthenticationContextHolder.getContext();
String username = usernamePasswordAuthenticationToken.getName();
String password = usernamePasswordAuthenticationToken.getCredentials().toString();
//记录用户登录次数
Integer retryCount = redisCache.getCacheObject(getCacheKey(username));
if (retryCount == null)
{
retryCount = 0;
}
if (retryCount >= Integer.valueOf(maxRetryCount).intValue())
{
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
MessageUtils.message("user.password.retry.limit.exceed", maxRetryCount, lockTime)));
throw new UserPasswordRetryLimitExceedException(maxRetryCount, lockTime);
}
//密码和用户名 不匹配 (user是数据库获取的,密码是加密过的)
if (!matches(user, password))
{
retryCount = retryCount + 1;
AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL,
MessageUtils.message("user.password.retry.limit.count", retryCount)));
//密码不正确 登录次数+1
redisCache.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
throw new UserPasswordNotMatchException();
}
else
{ //用户名和密码匹配 清空redis中的缓存
clearLoginRecordCache(username);
}
}
jwt认证过滤器
login方法完成了上述用户名和密码的验证后,生成token再把这个token返回给客户端,并该用户token作为key,其权限相关信息作为vaule存放再redis中。前端请求数据时携带token能够快速进行身份验证。
public String login(String username, String password, String code, String uuid)
{
//TODO 登录认证
....
//获取当前登录用户的信息
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
// 生成token
return tokenService.createToken(loginUser);
}
public String createToken(LoginUser loginUser)
{
//生成uuid作为token
String token = IdUtils.fastUUID();
loginUser.setToken(token);
//设置用户代理信息
setUserAgent(loginUser);
//更新redis中的信息 以token作为key loginUser作为value
refreshToken(loginUser);
//{"login_user_key":uuid} jwt编码生成返回给前端的token
Map<String, Object> claims = new HashMap<>();
claims.put(Constants.LOGIN_USER_KEY, token);
return createToken(claims);
}
当用户登录成功后,redis保存了token(uuid):loginUser这样的键值对信息,当前端携带token发送请求时,需要在SecurityConfig设置过滤器(即在UsernamePasswordAuthenticationFilter过滤器对用户信息认证前验证token的有效性
SecurityConfig
/**
* token认证过滤器
*/
@Autowired
private JwtAuthenticationTokenFilter authenticationTokenFilter;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
//TODO ...其他设置
// 添加JWT filter 把JwtAuthenticationTokenFilter对象添加在UsernamePasswordAuthenticationFilter过滤器前
httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
JwtAuthenticationTokenFilter
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException
{
//解析token 获取
LoginUser loginUser = tokenService.getLoginUser(request);
if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
{
//验证令牌有效期 相差不足20分钟,自动刷新缓存
tokenService.verifyToken(loginUser);
//存入SecurityContextHolder loginUser.getAuthorities()获取权限信息
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// 放入Authentication对象
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
//放行
//(如果没有token)未登录状态的时候 放行会被后面的security拦截下来进行登录相关操作(后面的过滤器会对其进行处理)
chain.doFilter(request, response);
}
退出登录
退出登录同样在SecurityConfig中配置
SecurityConfig
/**
* 退出处理类
*/
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception
{
//TODO ...其他设置
// 添加Logout filter
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
}
更多推荐
所有评论(0)