前言
在上一篇完成项目的构建后,完成了SpringSecurity获取用户信息和权限信息的逻辑。这一篇要实现SpringSecurity的配置,实现自己的登录认证和授权的逻辑。要完成自定义认证和授权的逻辑,就需要自己实现两个过滤器。
这两个过滤器是认证过滤器和接口访问过滤器。分别要继承UsernamePasswordAuthenticationFilter类和BasicAuthenticationFilter这两个类。在UsernamePasswordAuthenticationFilter的实现类中可以定义登录接口的路径以及登录成功和失败后对应的逻辑,其中获取用户信息的逻辑就是调用上篇写的loadUserByUsername方法。我准备在登录成功后生成一个token并存入redis,失败后就直接返回一段字符串。
认证过滤器
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private RedisTemplate redisTemplate;
public TokenLoginFilter(AuthenticationManager authenticationManager, RedisTemplate redisTemplate) {
this.authenticationManager = authenticationManager;
this.redisTemplate = redisTemplate;
this.setPostOnly(false);
// 设置登录路径匹配/autoperm/user/login
this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/autoperm/user/login","POST"));
}
/**
* 身份验证
* @param req
* @param res
* @return org.springframework.security.core.Authentication
* @author 黎勇炫
* @create 2022/6/10
* @email 1677685900@qq.com
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
throws AuthenticationException {
User user;
try {
user = new ObjectMapper().readValue(req.getInputStream(), User.class);
} catch (Exception e) {
throw new UserException(UserCodeEnum.AUTHENTICATION_FAILED);
}
// 调用UserDetailsServiceImpl.loadUserByUsername
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
}
/**
* 登录成功
* @param req
* @param res
* @param chain
* @param auth
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
Authentication auth) throws IOException {
User user = (User) auth.getPrincipal();
// 生成token
String token = JwtUtils.getJwtToken(user.getId(),user.getUsername());
// 将token信息存入redis缓存
redisTemplate.opsForValue().set(user.getUsername(), user.getPermissions());
ResponseUtils.write(res,R.ok().setData(token));
}
/**
* 登录失败
* @param request
* @param response
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
AuthenticationException e) throws IOException {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSON.toJSONString(R.error(HttpStatus.UNAUTHENTICATE,"认证失败")));
}
}
接口访问过滤器
接下来就编写接口访问过滤器,实现BasicAuthenticationFilter类。在这个类中要拿到认证过滤器中发的token,并根据token获取用户名。再依据用户名到redis中拿到用户的权限列表为用户授权。
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
private RedisTemplate redisTemplate;
public TokenAuthenticationFilter(AuthenticationManager authManager, RedisTemplate redisTemplate) {
super(authManager);
this.redisTemplate = redisTemplate;
}
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
throws IOException, ServletException {
if(req.getRequestURI().indexOf("admin") != -1) {
chain.doFilter(req, res);
return;
}
UsernamePasswordAuthenticationToken authentication = null;
try {
authentication = getAuthentication(req);
} catch (Exception e) {
throw new UserException(UserCodeEnum.TOKEN_NOT_FOUND);
}
if (authentication != null) {
SecurityContextHolder.getContext().setAuthentication(authentication);
} else {
throw new UserException(UserCodeEnum.AUTHENTICATION_FAILED);
}
chain.doFilter(req, res);
}
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
// token置于header里
String token = request.getHeader("token");
if (token != null && !"".equals(token.trim())) {
String userName = JwtUtils.getUsernameByJwtToken(request);
Set<String> permissionValueList = (Set<String>) redisTemplate.opsForValue().get(userName);
Collection<GrantedAuthority> authorities = new ArrayList<>();
if(!CollectionUtils.isEmpty(permissionValueList)){
for(String permissionValue : permissionValueList) {
if(StringUtils.isEmpty(permissionValue)) continue;
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
}
if (!StringUtils.isEmpty(userName)) {
return new UsernamePasswordAuthenticationToken(userName, token, authorities);
}
return null;
}
return null;
}
}
到这里就完成了用户的登录认证和接口访问授权了。这样用户只要一访问接口就自动实现了权限配置。
登出处理器
登出处理器LogoutSuccessHandler需要继承LogoutHandler类,逻辑很简单,只需要拿到token并从redis中删除对应的信息就可以了。
@Component
public class LogoutSuccessHandler implements LogoutHandler {
private RedisTemplate redisTemplate;
public LogoutSuccessHandler(RedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String token = request.getHeader("token");
if (token != null)
//清空当前用户缓存中的权限数据
{
String userName = JwtUtils.getUsernameByJwtToken(request);
redisTemplate.delete(userName);
}
PrintWriter writer = null;
try {
writer = response.getWriter();
} catch (IOException e) {
e.printStackTrace();
}
writer.write(JSON.toJSONString(R.ok()));
writer.close();
}
}
未授权(权限不足)处理器
在这个处理器中做出权限不足或未授权时的业务逻辑,我这简单的返回对应的信息。
/**
* 未授权统一处理
* @author 黎勇炫
* @date 2022年06月10日 11:04
*/
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setStatus(200);
response.setContentType("application/json");
response.setCharacterEncoding("utf-8");
response.getWriter().print(JSON.toJSONString(R.error(HttpStatus.UNAUTHORIZED,"权限不足")));
}
}
配置Security
将刚才编写的过滤器和处理器以及密码加密注册到Springsecurity中,并配置路径的访问权限,登录接口时可以匿名访问的。
/**
* 安全框架配置类
* @author 黎勇炫
* @date 2022年06月10日 9:36
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserServiceImpl userService;
@Autowired
private LogoutSuccessHandler logoutSuccessHandler;
@Autowired
private RedisTemplate redisTemplate;
/**
* 密码加密
* @return org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder
* @author 黎勇炫
* @create 2022/6/10
* @email 1677685900@qq.com
*/
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 设置用户登录校验逻辑和加密算法(强散列哈希)
* @param auth
* @return void
* @author 黎勇炫
* @create 2022/6/10
* @email 1677685900@qq.com
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(bCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
// 未授权处理
.authenticationEntryPoint(new UnauthorizedEntryPoint())
// 关闭csrf
.and().csrf().disable()
// 需要授权的请求
.authorizeRequests()
.antMatchers("/autoperm/user/login").anonymous()
.anyRequest().authenticated()
// 退出登录的请求路径和对应的处理器
.and().logout().logoutUrl("/autoperm/user/logout")
.addLogoutHandler(new LogoutSuccessHandler(redisTemplate)).and()
// 登录过滤器
.addFilter(new TokenLoginFilter(authenticationManager(), redisTemplate))
// 接口认证过滤器
.addFilter(new TokenAuthenticationFilter(authenticationManager(), redisTemplate)).httpBasic();
}
}
到这里已经实现了基本的登录逻辑和接口访问逻辑。下一篇就整合前端实现登录已经动态菜单。