提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
一、什么SpringSecurity?
SpringSecurity是一个提供认证、授权和防止常见攻击的框架。 它提供了保护命令式和响应式应用程序的一流支持,是保护基于spring的应用程序的事实上的标准。
主要功能
- 认证:验证当前访问用户的是不是本系统的用户,并且要确定具体是哪个用户
- 授权:经过认证后判断当前用户是否有权限进行操作
原理
SpringSecurity的原理其实就是一个过滤器,内部包含了提供各种功能的过滤器
二、使用步骤
1.引入依赖
代码如下(示例):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
导入依赖后的页面,会跳转到SpringSecurity的默认登录页面
2.认证流程分析
根据SpringSecurity的认证流程进行设计
配置类SecurityConfig
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecutityConfig extends WebSecurityConfigurerAdapter {
/**
* 用户认证过滤器
*/
@Autowired
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
/**
* 认证失败处理类
*/
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
/**
*
*/
@Autowired
private AccessDeniedHandler accessDeniedHandler;
/**
* 加密方法
* @return
*/
@Bean
public PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception{
httpSecurity
//不使用session,禁用CSRF
.csrf().disable()
//不通过session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
//允许登录接口匿名访问
.antMatchers("/login").anonymous()
//除此之外的所有接口需要鉴权认证
.anyRequest().authenticated();
//添加JWT过滤器在UsernamePasswordAuthenticationFilter之前
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
1.在登录接口调用ProviderManager
//Authentication authticate对用户进行认证
Authentication authentication = null;
//该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username,password));
2.自定义UserDetailsService
2.1实现UserDetails接口,封装用户信息
public class LoginUser implements UserDetails {
private SysUser user;
public LoginUser(SysUser user,List<String> permissions) {
this.permissions = permissions;
this.user = user;
}
@Override
private List<GrantedAuthority> authorities;
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
authorities = permissions.stream().
map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
}
2.2重写loadUserByUsername方法,改为在数据库中查询用户信息和权限
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
ISysUserService userService;
@Autowired
SysMenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
SysUser user = userService.selectUserByUserName(username);
if (user == null){
throw new UsernameNotFoundException("该用户不存在");
}
//获取用户权限
List<String> auth = menuMapper.selectPermsByUserId(user.getUserId());
return new LoginUser(user,auth);
}
}
3.将返回的UserDetails对象通过PasswordEncoder进行对比
在登录过程中对密码进行加密,使用时只需要把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
4.认证成功后,返回jwt,将用户信息存储到redis中,以后的请求头携带该jwt
public String login(String username,String password) {
//Authentication authticate对用户进行认证
Authentication authentication = null;
//该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username,password));
if (Objects.isNull(authentication)) {
throw new RuntimeException("认证失败");
}
//取到用户userID
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
String userId = loginUser.getUser().getUserId().toString();
//根据用户Id生成JWT
String jwt = JwtUtil.createJWT(userId);
//authenticate存入redis
redisCache.setCacheObject("login:"+userId, loginUser);
//设置过期时间
redisCache.expire("login:"+userId, 1800);
return jwt;
}
5.配置认证过滤器
对请求头中的token进行解析,将解析到的userId到redis中进行查询,将查询到的LoginUser封装成Authentication对象存入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
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
6.退出登录
获取SecurityContextHolder中的认证信息,删除redis中对应的数据即可。
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,"退出成功");
}
3.授权过程分析
基本流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
1.在LoginUser中添加权限信息
public class LoginUser implements UserDetails {
......
//存储权限信息
private List<String> permissions;
//存储SpringSecurity所需要的权限信息的集合
@JSONField(serialize = false)
private List<GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
//把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
authorities = permissions.stream().
map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
}
2.在UserDetailsServiceImpl把权限信息封装到LoginUser
public class UserDetailsServiceImpl implements UserDetailsService {
......
//获取用户权限
List<String> auth = menuMapper.selectPermsByUserId(user.getUserId());
return new LoginUser(user,auth);
}
3.对认证过滤器中对token进行解析,将权限封装到Authentication中
解析token获取UserLogin对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
4.加上权限注解@PreAuthorize
需要有system:role:query权限才能操作
@PreAuthorize("hasAuthority('system:role:query')")
@GetMapping(value = "/{roleId}")
public AjaxResult getInfo(@PathVariable Long roleId)
{
return AjaxResult.success(roleService.selectRoleById(roleId));
}
总结
以上内容是对SpringBoot整合SpringSecurity一些基础的操作,SpringSecurity还可以根据具体需求自定义失败处理,登出处理等,总之十分好用,还需不断探索。