配置类:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {// 已登录的用户的token验证@Autowiredprivate JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;// 登录失败@Autowiredprivate AuthenticationEntryPointImpl authenticationEntryPoint;// 认证失败@Autowiredprivate AccessDeniedHandlerImpl accessDeniedHandler;// 前端用户登录的用户信息验证@Bean@Overridepublic AuthenticationManager authenticationManagerBean() throws Exception {return super.authenticationManagerBean();}// 密码加密解密@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder();}@Overrideprotected void configure(HttpSecurity http) throws Exception {// super.configure(http);http// 关闭csrf.csrf().disable()// 不通过session获取SecurityContext.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()// 放行.antMatchers("/login").anonymous()// 这个接口需要认证才能访问.antMatchers("/link/getAllLink").authenticated().antMatchers("/logout").authenticated()// 其余的接口也放行.anyRequest().permitAll();// 关闭security的默认关闭接口http.logout().disable();// 配置过滤器http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);// 配置异常处理器http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler);// 允许跨域http.cors();}
}
用户登录流程:
@Service
public class BlogLoginServiceImpl implements BlogLoginService {@Autowiredprivate AuthenticationManager authenticationManager;@Autowiredprivate RedisTemplate<Object, Object> redisTemplate;@Overridepublic ResponseResult login(User user) {// 放入用户名和密码UsernamePasswordAuthenticationToken authentication =new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());// authenticate 会调用 UserDetailsServiceImpl 登录判断Authentication authenticate = authenticationManager.authenticate(authentication);// 判断是否认证通过if (authenticate == null) throw new RuntimeException("用户名或密码错误");// 获取userId 生成tokenLoginUser loginUser = (LoginUser) authenticate.getPrincipal();Long userId = loginUser.getUser().getId();String token = JwtUtil.createJWT(String.valueOf(userId));// 用户信息存入redisredisTemplate.opsForValue().set("bloglogin:" + userId, loginUser,1, TimeUnit.DAYS);// token和userinfo封装 返回BlogUserLoginVo vo = new BlogUserLoginVo();vo.setToken(token);vo.setUserInfo(BeanCopyUtils.copyBean(loginUser.getUser(), UserInfoVo.class));return ResponseResult.success(vo);}
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {@Autowiredprivate UserMapper userMapper;// 登录判断@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {if (StringUtils.isEmpty(username)) throw new RuntimeException("用户名不能为空");// 条件LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();wrapper.eq(User::getUserName, username);// 查询User user = userMapper.selectOne(wrapper);if (user == null) throw new RuntimeException("用户不存在");return new LoginUser(user);}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {private User user;@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {// 权限集合return null;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getUserName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return true;}
}
用户登陆后,访问权限接口,token认证拦截器:
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {// 用户访问需要登录的接口,验证token@Autowiredprivate ObjectMapper om;@Autowiredprivate RedisTemplate redisTemplate;@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {// 获取tokenString token = request.getHeader("token");if (!StringUtils.hasText(token)) {// 后面登录的security还会进行校验filterChain.doFilter(request, response);return;}// 解析token 获取userIdClaims claims = null;try {claims = JwtUtil.parseJWT(token);} catch (Exception e) {e.printStackTrace();// 过期// 非法token// 响应前端重新登录ResponseResult result = new ResponseResult(401, "用户未登录", null);WebUtils.renderString(response, om.writeValueAsString(result));return;}String userId = claims.getSubject();// 从redis中获取用户信息LoginUser loginUser = (LoginUser) redisTemplate.opsForValue().get("bloglogin:" + userId);if (loginUser == null) {// redis里没有ResponseResult result = new ResponseResult(402, "登录已过期", null);WebUtils.renderString(response, om.writeValueAsString(result));return;}// 存入SecurityContextHolder// 未认证的是2个参数,已认证的是3个参数UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(loginUser, null, null);SecurityContextHolder.getContext().setAuthentication(authentication);// 放行filterChain.doFilter(request, response);return;}
}
以上完成后,拦截后不会返回前端json结果
自定义认证失败处理:
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {@Autowiredprivate ObjectMapper om;@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {// 登录失败失败响应给前端authException.printStackTrace();// BadCredentialsException 用户名或密码错误// InsufficientAuthenticationException 需要登录ResponseResult result = null;if (authException instanceof BadCredentialsException)result = new ResponseResult(401, authException.getMessage(), null);else if (authException instanceof InsufficientAuthenticationException)result = new ResponseResult(402, "用户未登录", null);elseresult = new ResponseResult(403, authException.getMessage(), null);WebUtils.renderString(response, om.writeValueAsString(result));}
}
下面这个是鉴权的响应,目前还用不到:
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {@Autowiredprivate ObjectMapper om;@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {// 鉴权失败的响应accessDeniedException.printStackTrace();ResponseResult result = new ResponseResult(401, "无权限操作", null);WebUtils.renderString(response, om.writeValueAsString(result));}
}
统一异常处理:
@Data
@NoArgsConstructor
public class SystemException extends RuntimeException {private Integer code;private String msg;public SystemException(Integer code, String msg) {super(msg);this.code = code;this.msg = msg;}
}
// 捕获Controller的异常
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {// 可控的异常@ExceptionHandler(SystemException.class)public ResponseResult SystemExceptionHandler(SystemException e) {// 打印日志log.error("出现了异常! {}", e);// 响应前端return new ResponseResult(e.getCode(), e.getMsg(), null);}// 不可控的异常@ExceptionHandler(Exception.class)public ResponseResult exceptionHandler(Exception e) {// 打印日志log.error("出现了异常! {}", e);// 响应前端return new ResponseResult(500, e.getMessage(), null);}
}
参考视频:
B站最通俗易懂手把手SpringBoot+Vue项目实战-前后端分离博客项目-Java项目_哔哩哔哩_bilibili