Spring Security是基于Spring框架的权鉴框架,提供了一套WEB应用安全性的完整解决方案
SpringSecurity的两个重要核心功能就是用户认证(Authentication)和用户授权(Authorization)
RBAC模型就不多赘述了,就是将权限从用户变成了权限到角色再到用户
可以把SpringSecurity看作是Spring的过滤链FilterChain中的一环,而我们想要使用SpringSecurity就是要自定义一部分过滤器
Security Filters
上图显示的是Security的Filter Chain,在正式开发中我们只需要关注三个重点Filter
UsernamePasswordAuthenticationFilter:负责用户的认证功能
ExceptionTranslationFilter:负责处理过滤器链中的AccessDeniedException和AuthenticationException异常
FilterSecurityInterceptor:负责权限校验的处理器
上图是SpringSecirity中认证的流程,我们要使用Security就需要做下述步骤
创建一个登录接口如/user/login
调用AuthenticationManager的authenticate方法进行认证,这里的AutnenticationManager,我们可以创建一个SecurityConfig,集成WebSecurityConfigurerAdapter
后,重载authenticationManagerBean()
方法,可以通过AuthenticationManager调用authenticate()
进行认证
这里的authenticate()
需要的参数为Authentication
接口的实现类,可以使用实现类 UsernamePasswordAuthenticationToken
,这里的principle和credientials就是用户名和密码
认证通过后会返回一个Authentication对象,如果对象为null说明认证失败了,抛出一个异常即可
此时返回的Authentication对象中的principle变成了用户的信息
使用jwt工具类将用户信息的userid
转化为token
将用户信息(包括权限信息)以userid:用户信息的kv格式保存到Redis数据库中
返回给前台由userid转换的token
前端将token保存在LocalStorage中同时每次访问都需要在Header中携带token
此外我们还需要实现UserDetailService
,也就是图中的第五步
创建类实现UserDetailService
,实现loadUserByUsername()
方法,从数据库中获取用户信息以及权限信息
由于返回对象需要是UserDetails
接口的实现类,所以我们自己实现一个LoginUser类
自己配置之后的时序图
在Security中的密码,我们选用官方推荐的BCryptPasswordEncoder
,只需要在配置类中配置即可
这里直接贴完整的带着权限校验的配置文件
要使用注解进行权限校验的话需要注解@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true) // 打开基于注解的权限控制方案 public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; @Autowired private AccessDeniedHandler accessDeniedHandler; @Autowired private AuthenticationEntryPoint authenticationEntryPoint; // 密码校验 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable() // 不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers("/user/login").anonymous() // 对于登录接口 允许匿名登录 .anyRequest().authenticated(); // 其余接口全都要权鉴 // 将过滤器添加到UsernamePasswordAuthenticationFilter前 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 配置异常处理器 http.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler); // 开启跨域 http.cors(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }}
loginServiceImpl
@Servicepublic class LoginServiceImpl implements LoginService { @Autowired private AuthenticationManager authenticationManager; @Autowired private RedisCache redisCache; @Override public ResponseResult login(User user) { // AuthenticationManager authenticate进行认证 Authentication authenticate = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword())); // 如果认证没通过,给出对应的提示 if (Objects.isNull(authenticate)){ throw new RuntimeException("登录失败!"); } // 如果认证通过了,使用userid生成一个jwt,jwt存入ResponseResult返回 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); String userid = loginUser.getUser().getId().toString(); String jwt = JwtUtil.createJWT(userid); HashMap<String, String> map = new HashMap<>(); map.put("token",jwt); // 把完整的用户信息存入redis,userid作为key redisCache.setCacheObject("login:"+userid,loginUser); return new ResponseResult(200,"登陆成功",map); } @Override public ResponseResult logout() { // 获取SecurityContextHolder中的用户id UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); Long userid = loginUser.getUser().getId(); // 删除redis中的值 redisCache.deleteObject("login:" + userid.toString()); return new ResponseResult(200,"注销成功"); }}
实现了UserDetails的LoginUser
@Data@NoArgsConstructorpublic class LoginUser implements UserDetails { private User user; private List<String> permissions; @JSONField(serialize = false) private List<SimpleGrantedAuthority> authorities; public LoginUser(User user, List<String> permissions) { this.user = user; this.permissions = permissions; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (authorities != null) return authorities; // 把permissions中的权限信息封装成SimpleGrantedAuthority对象 authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList()); return authorities; } @Override public String getPassword() { return user.getPassword(); } @Override public String getUsername() { return user.getUserName(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; }}
实现了UserDetailsService的UserDetailsServiceImpl
@Servicepublic class UserDetailsServiceImpl implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private MenuMapper menuMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>(); queryWrapper.eq(User::getUserName,username); // 查询用户信息 User user = userMapper.selectOne(queryWrapper); // 如果没有查询到用户,就抛出异常 if (Objects.isNull(user)){ throw new RuntimeException("用户名不存在"); } // TODO 查询对应的权限信息 List<String> list = menuMapper.selectPermsByUserId(user.getId()); // 把数据封装成UserDetails返回 return new LoginUser(user,list); }}
定义JWT认证过滤器
Jwt认证过滤器放在UsernamePasswordAuthenticationFilter之前
获取token
解析token获取其中的userid
从 Redis中获取用户信息
存入SecurityContextHolder,放入SecurityContextHolder中的用户信息,在整个线程中都可以被使用
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired private 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 // TODO 获取权限信息封装到authenticationToken中 UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authenticationToken); filterChain.doFilter(request,response); }}
将过滤器配置在UsernamePasswordAuthenticationFilter
之前
// 将过滤器添加到UsernamePasswordAuthenticationFilter前 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
使用注解方法进行方法的权鉴@PreAuthorize
内有多个方法可以查看SecurityExpressionRoot
类
@GetMapping("/hello") @PreAuthorize("hasAuthority('sys:book:list')") public String hello(){ return "hello"; }
也可以自定义方法
@Component("ex") public class MyExpressionRoot { public boolean hasAuthority(String authority){ // 获取当前用户权限 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); List<String> permissions = loginUser.getPermissions(); return permissions.contains(authority); }}
@GetMapping("/hello") @PreAuthorize("@ex.hasAuthority('sys:book:list')") public String hello(){ return "hello"; }
校验失败处理器
@Component public class AccessDeniedHandlerImpl implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.FORBIDDEN.value(), "您的权限不足"); String json = JSON.toJSONString(result); WebUtils.renderString(httpServletResponse,json); } }
登录失败处理器
@Component public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(),"用户认证失败,请重新登录!"); String json = JSON.toJSONString(result); WebUtils.renderString(httpServletResponse,json); } }
两种处理器都需要的配置文件中注册
// 配置异常处理器 http.exceptionHandling() .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler);
跨域
http.cors();