SpringSecurity学习笔记
前言
上篇文章也说了,这两天我又把SpringSecurity
看了一遍,记得之前看的时候就只看到加密然后被一个bug
卡了一天就去看别的了,现在重新看顺便把之前没做完的笔记补一补,下面基本是根据三更草堂的笔记整理过来的
完整流程图
SpringSecurity
的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器
UsernamePasswordAuthenticationFilter
:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter
:处理过滤器链中抛出的任何AccessDeniedException
和AuthenticationException
。
FilterSecurityInterceptor
:负责权限校验的过滤器。
Authentication
接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager
接口:定义了认证Authentication
的方法
UserDetailsService
接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails
接口:提供核心用户信息。通过UserDetailsService
根据用户名获取处理的用户信息要封装成UserDetails
对象返回。然后将这些信息封装到Authentication
对象中。
SecurityConfig配置
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
AuthenticationEntryPointImpl authenticationEntryPoint;
@Autowired
AccessDeniedHandlerImpl accessDeniedHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
//把token校验过滤器添加到过滤器链中
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//全局自定义异常处理
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
//允许跨域
http.cors();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
认证
自定义登录接口
认证成功的话要生成一个jwt
,放入响应中返回。并且为了让用户下回请求时能通过jwt
识别出具体的是哪个用户,我们需要把用户信息存入redis
,可以把用户id
作为key
。
@RestController
public class LoginController {
@Autowired
private LoginService loginService;
@PostMapping("/user/login")
public ResponseResult login(@RequestBody User user){
return loginService.login(user);
}
@RequestMapping("/user/logout")
public ResponseResult logout(){
return loginService.logout();
}
}
@Service
public class LoginServiceImpl implements LoginService {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
RedisCache redisCache;
@Override
public ResponseResult login(User user) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
if (Objects.isNull(authenticate)) {
throw new RuntimeException("登录失败");
}
//使用userid生成token
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
String userid = loginUser.getUser().getId().toString();
String jwt = JwtUtil.createJWT(userid);
//authenticate存入redis
redisCache.setCacheObject("login:" + userid, loginUser);
//把token响应给前端
HashMap<String, String> map = new HashMap<>();
map.put("token", jwt);
return new ResponseResult(200, "登录成功", map);
}
@Override
public ResponseResult logout() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
Long userid = loginUser.getUser().getId();
redisCache.deleteObject("login:" + userid);
return new ResponseResult(200,"注销成功");
}
}
自定义认证过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response);
return;
}
//解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)) {
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
//获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
filterChain.doFilter(request, response);
}
}
授权
需要的表如下
我觉得比较重要的字段是功能权限信息,表结构如下
限制访问资源所需权限
我们可以使用注解去指定访问对应的资源所需的权限(主要使用)
先开启权限配置
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
然后就可以使用对应的注解。@PreAuthorize
@RestController
public class HelloController {
@GetMapping("/hello")
@PreAuthorize("hasAuthority('system:dept:list')")
public String hello(){
return "hello";
}
}
封装权限信息
下面具体看一下UserDetails
的实现类LoginUser
的getAuthorities
方法的实现,这里使用了Java8
的新特性流,当然注释里的方法也是可行的
@JSONField(serialize = false)
List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null){
return authorities;
}
// authorities = new ArrayList<>();
// for (String permission : permissions) {
// SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permission);
// authorities.add(authority);
// }
authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
查询权限
这里放一下我写的SQL
,三更用的是显式,但我更喜欢隐式哈哈哈
select
distinct m.perms
from
sys_role as r, sys_menu as m, sys_user_role as ur, sys_role_menu as rm
where
ur.role_id = rm.role_id and rm.menu_id = m.id and ur.user_id = #{userid}
然后我们可以在UserDetailsServiceImpl
中去调用该mapper
的方法查询权限信息封装到LoginUser
对象中即可,代码在上面的UserDetailsServiceImpl
已经放过了这里就不再放了
自定义失败处理
如果是认证过程中出现的异常会被封装成AuthenticationException
然后调用AuthenticationEntryPoint
对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException
然后调用AccessDeniedHandler
对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义AuthenticationEntryPoint
和AccessDeniedHandler
然后配置给SpringSecurity
即可。
这里就放其中一个,另一个写法一致,还要记得在SecurityConfig
中配置这两个实现类
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "您的权限不足!");
String json = JSON.toJSONString(result);
WebUtils.renderString(response, json);
}
}
跨域
一是对SpringBoot
配置,二是在SecurityConfig
中配置(添加http.cors()
即可)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
认证处理器
实际上在UsernamePasswordAuthenticationFilter
进行登录认证的时候,如果登录成功了是会调用AuthenticationSuccessHandler
的方法进行认证成功后的处理,失败则是调用AuthenticationFailureHandler
,实现这两个接口即可
补充一下还有一个登出成功处理器,实现接口是LogoutSuccessHandler
然后在SecurityConfig
中配置
http.formLogin()
//配置认证成功处理器
.successHandler(successHandler)
//配置认证失败处理器
.failureHandler(failureHandler);
http.logout()
//配置注销成功处理器
.logoutSuccessHandler(logoutSuccessHandler);