SpringSecurity简单使用的总结
SpringSecurity的作用:
核心功能:实现对web应用的访问进行认证与授权
- 认证:验证当前访问系统的用户是不是合法的用户,并且确认具体的身份
- 授权:在经过认证以后,判断当前用户是否具有权限进行某个操作
快速入门
建立springBoot工程,导入需要的依赖
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.noloafing</groupId>
<artifactId>springSecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springSecurity</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- security依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- springBoot热部署依赖-->
<!--<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>-->
<!-- 数据库连接依赖-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- MybatisPlus依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.1</version>
</dependency>
<!-- lombok依赖 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- JUnit依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--fastjson依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.33</version>
</dependency>
<!--jwt依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
</dependencies>
之后创建启动类
@SpringBootApplication
public class SecurityApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication.class,args);
}
}
写一个响应的案例进行测试:
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello(){
return "hello SpringBoot!";
}
}
SpringSecurity引入成功的结果:
会在访问对应端口的资源路径时跳转到**/login**一个登录页进行登录,用户名 user 密码显示在控制台
登录成功之后springSecurity自己做了一系列认证的处理,之后访问到**/hello**下的资源
以下内容涉及的 jwtUtils加密工具类 webUtils响应格式类 redis简单封装类 mapper类代码没有贴出
认证和授权
1. jwt实现登录校验流程
前端 ————> (携带用户名和密码)访问登录接口
————> 服务器(后端接收并从数据库获取数据处理业务逻辑)
————> 如果成功则将用户相关信息(userID)使用jwt返回token给前端
——————> 登录之后的其他请求都需要携带 token
—————>在请求中解析token如果正确就能拿到相关信息(userId)正常响应————>前端
2. SpringSecuirty流程
SpringSecuirty原理是一个过滤器链,而核心的几个过滤器分别是:
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor 负责权限校验的过滤器。
2.1使用springSecurity实现认证和授权:
- UsernamePasswordAuthenticationFilter 将用户名和密码封装到Authentication对象
- 调用AuthenticationManager中的authenticate认证方法
- 实现UserDetails接口方法,将数据库中的用户密码等信息拿到返回UserDetails对象
- authentication对象中的用户密码信息通过方法与UserDetails中的校验,**如果成功,就将UserDetails中详细信息(包括权限信息)**封装到Authentication对象中,如果失败则进入异常处理
- 返回Authentication对象,同时使用SecurityContextHolder进行键值对形式存储
- 用户登录成功之后访问接口时,过滤器会获取SecurityContextHolder中的authentication对象,其中包括(User,Permissions)信息,之后对应的api通过其方法上的
@PreAuthorize("hasAnyAuthority('xxx权限')")
注解决定用户是否具有权限
步骤:
-
定义配置类:SecurityConfig:springSecurity相关的配置都在这里定义
@Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) //开启授权相关配置 public class SecurityConfig{ //Jwt认证过滤处理器 @Resource private JwtAuthenticationFilter jwtAuthenticationFilter; //认证异常处理 @Resource private AuthenticationEntryPoint authenticationEntryPoint; //授权异常处理 @Resource private AccessDeniedHandler accessDeniedHandler; //创建BCryptPasswordEncoder对象加密密码,注入到容器,解决SpringSecurity默认的密码校验方式 @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } /** * 获取AuthenticationManager(认证管理器)进行登录的认证 */ @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } @Bean SecurityFilterChain filterChain(HttpSecurity http) throws Exception{ return http//关闭csrf .csrf().disable() //不通过Session获取SecurityContext .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() // 对于登录接口 允许匿名访问 .antMatchers("/user/login").anonymous() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() //添加Jwt认证过滤器(自定义的类,实现对所有请求的先行过滤) .and().addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class) //配置异常处理器 .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler) .and().build(); } }
csrf
CSRF是指跨站请求伪造(Cross-site request forgery),是web常见的攻击之一。
SpringSecurity去防止CSRF攻击的方式就是通过csrf_token。后端会生成一个csrf_token,前端发起请求的时候需要携带这个csrf_token,后端会有过滤器进行校验,如果没有携带或者是伪造的就不允许访问。
CSRF攻击依靠的是cookie中所携带的认证信息。但是在前后端分离的项目中我们的认证信息其实是token,而token并不是存储中cookie中,并且需要前端代码去把token设置到请求头中才可以,所以CSRF攻击也就不用担心了。
数据库设计
-
实现UserDetail对象为(LoginUser)
@Data @NoArgsConstructor public class LoginUser implements UserDetails { private User user; //定义权限信息 private List<String> permissions; //拿到权限信息进行封装 //把用户和对应权限进行一个封装(因为这里数据库中权限和用户是分别管理的所以这样封装) public LoginUser(User user,List<String> permissions){ this.user = user; this.permissions = permissions; } //存储SpringSecurity所需要的权限信息的集合 @JSONField(serialize = false) //防止在序列化时因为Security的这个配置而报错 //定义授权信息集合 private List<GrantedAuthority> authorities; //返回授权的一些数据 @Override public Collection<? extends GrantedAuthority> getAuthorities() { if (authorities != null){ return authorities; } //字符串形式的Permissions集合封装为GrantedAuthority的继承对象形式 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; } }
-
实现UserDetailService接口,重写其中的方法(loadUserByUsername)
@Service public class UserDetailsServiceImpl implements UserDetailsService { @Resource private UserMapper userMapper; //对应用户信息查询的Mapper @Resource private MenuMapper menuMapper; //对应权限查询的Mapper @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("用户名或者密码错误"); } // 从数据库查询用户的权限信息 List<String> perms = menuMapper.selectPermsByUserId(user.getId()); // 数据封装到UserDetails返回 return new LoginUser(user,perms); } }
-
通过1.2,目前已经封装好了用户的核心相关信息,之后需要实现LoginService登录业务逻辑
ResponseResult类:
@JsonInclude(JsonInclude.Include.NON_NULL) public class ResponseResult<T> { /** * 状态码 */ private Integer code; /** * 提示信息,如果有错误时,前端可以获取该字段进行提示 */ private String msg; /** * 查询到的结果数据, */ private T data; public ResponseResult(Integer code, String msg) { this.code = code; this.msg = msg; } public ResponseResult(Integer code, T data) { this.code = code; this.data = data; } public Integer getCode() { return code; } public void setCode(Integer code) { this.code = code; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } public T getData() { return data; } public void setData(T data) { this.data = data; } public ResponseResult(Integer code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } }
LoginServiceImpl
@Service public class LoginServiceImpl implements LoginService { @Autowired private AuthenticationManager authenticationManager;//提供认证方法的对象实例 @Resource private RedisCache redisCache; //使用redis缓存用户的核心信息,减少多次调用数据库 @Resource private UserMapper userMapper; /** * 实现登录业务逻辑 * @param user */ @Override public ResponseResult login(User user) { // 利用 AuthenticationManager authenticate 进行用户认证 //利用该方法接收输入的用户名与密码 生成token UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword()); //认证 这个方法会最终去调用认证相关的方法,与我们重写的UserDetail对象中的信息比较,返回认证结果 Authentication authenticate = authenticationManager.authenticate(authenticationToken); // 如果认证失败 给出提示 if(Objects.isNull(authenticate)){ throw new RuntimeException("认证未通过,登录失败"); } // 认证成功 获取用户的userId,使用JWT 存入ResponseResult(一个自定义的返回JSON格式数据的封装类)返回 LoginUser loginUser = (LoginUser) authenticate.getPrincipal(); //拿到放行后的LoginUser String userId = loginUser.getUser().getId().toString(); //拿到userId String jwt = JwtUtil.createJWT(userId); //使用jwt对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中的userId Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); LoginUser loginUser = (LoginUser) authentication.getPrincipal(); String userId = loginUser.getUser().getId().toString(); //在Redis中清除缓存 redisCache.deleteObject("login:"+userId); return new ResponseResult(200, "注销成功"); } }
这里有个小问题:连续访问登录生成的几个token都能用于登录,因为redis中key仅是userId,需要考虑唯一性,还有token时效性,这里没有做处理
-
添加 JWT 认证过滤器 作用:在Authentication过滤器之前进行状态和权限的鉴别,即对登录认证和拿到token之后的权限获取
OncePerRequestFilter:
只处理一次请求的过滤器
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
/**
* 在Authentication(User)过滤器之前进行状态和权限的鉴别
*
*/
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader("token");
//判断token是否为空
if (!StringUtils.hasText(token)){
//放行 由最后的过滤器处理
filterChain.doFilter(request, response);
return; //防止该过滤器在response中被调用
}
//如果token不为空
String userId;
try {
//解析token中的userId
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
} catch (Exception e) {
throw new RuntimeException("token非法");
}
//如果解析成功拿到userId,从redis中获取登录后的用户信息
LoginUser loginUser = redisCache.getCacheObject("login:" + userId);
//如果用户为空
if (Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//获取权限封装到authentication中
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
//存入SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
- 配置接口授权:在对应接口的方法上加上对应注解 @PreAuthorize具体的授权用法除了hasAuthority方法还有一些使用方式一样的用法:( hasAnyAuthority,hasRole,hasAnyRole)
@RestController
public class HelloController {
@RequestMapping("/hello")
@PreAuthorize("hasAnyAuthority('system:book:list1')") //验证是否拥有对应授权
public String hello(){
return "hello SpringBoot!";
}
}
-
认证、授权异常处理
在SpringSecurity中,如果我们在认证或者授权的过程中出现了异常会被ExceptionTranslationFilter捕获到。
在ExceptionTranslationFilter中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程出现的异常会被封装成AuthenticationException然后调用AuthenticationEntryPoint对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成AccessDeniedException然后调用AccessDeniedHandler对象的方法去进行异常处理。
所以只需要自定义AuthenticationEntryPoint和AccessDeniedHandler然后配置给SpringSecurity即可。
代码如下:
@Component
public class AccessDenieHandlerImpl 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);
}
}
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
//处理认证异常
ResponseResult result = new ResponseResult(HttpStatus.UNAUTHORIZED.value(), "用户认证失败,请重新登录");
String json = JSON.toJSONString(result);
WebUtils.renderString(response, json);
}
}
配置:在SpringSecurity配置类中的SecurityFilterChain方法中添加:.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).accessDeniedHandler(accessDeniedHandler)
以上是对SpringSecurity的一个简单案例使用,实际上使用SpringSecurity处理认证与授权的核心功能的设计方案有多种,也可以直接使用自带的认证成功处理器( AuthenticationSuccessHandler)、认证失败处理器(AuthenticationFailureHandler),注销成功处理器(LogoutSuccessHandle)等,还可以自定义权限校验的方法:
例如写一个校验类:
@Component("perm")
public class MyRootExp {
public boolean hasAuthority(String authority){
//获取当前用户的权限
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();
List<String> permissions = loginUser.getPermissions();
//判断用户权限集合中是否存在authority
return permissions.contains(authority);
}
}
在对应的方法上使用表达式:
@RequestMapping("/hello")
@PreAuthorize("@perm.hasAuthority('admin')")
public String hello(){
return "helloSpringSecurity";
}
以上只涉及SpringSecurity一个简单的使用,要想熟练使用还是需要见识见识其他认证方案的实现和了解相关源码的原理实现