# 快速开始
# 引入 spring security
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-security --> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-security</artifactId> | |
<version>2.6.6</version> | |
</dependency> |
引入之后访问任何接口都会跳转到 spring security 提供的登陆页面,默认用户名 user,密码控制台会输出
# 认证
# 登录校验流程
# spring security 完整流程
SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器
图中只展示了核心过滤器,其他非核心过滤器没有展示
- UsernamePasswordAuthenticationFilter:此过滤器是最常用的身份验证过滤器,也是最常定制的过滤器,负责处理在登录页面填写的用户名密码
- ExceptionTranslationFilter:处理过滤器中抛出的 AccessDeniedException 或 AuthenticationException 异常
- FilterSecurityInterceptor:负责权限校验的过滤器
我们可以 debug 查看过滤器链的先后顺序,一共有十五个
# 认证流程详解
Authentication 接口:它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager 接口:定义了认证 Authenticationl 的方法
UserDetailsService 接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails 接口:提供核心用户信息。通过 UserDetailsService 根据用户名获取处理的用户信息要封装成 UserDetails 对象返回。然后将这些信息封装到 Authentication 对象中。
# 思路分析
登录:
1. 自定义登录接口
调用 ProviderManager 的方法进行认证 如果认证通过生成 jwt
把用户信息存入 redis 中
2. 自定义 UserDetailService
在这个实现类中查询数据库
检验:
1. 定义 jwt 认证过滤器
获取 token
解析 token 获取 userid
从 redis 中获取信息
存入 SecurityContextHolder 中
# 自定义 UserDetailService 在这个实现类中查询数据库
我们可以自定义一个 UserDetailsService,让 springSecurity 使用我们的 UserDetailService,我们自己的 UserDetailService 可以从数据库中查询数据库用户名密码
创建一个类实现 UserDetailsService 接口,重写其中的方法。更加用户名从数据库中查询用户信息
@Service | |
public class UserDetailsServiceImpl implements UserDetailsService { | |
@Autowired | |
private UserMapper userMapper; | |
@Override | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
// 根据用户名查询用户信息 | |
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); | |
wrapper.eq(User::getUserName,username); | |
User user = userMapper.selectOne(wrapper); | |
// 如果查询不到数据就通过抛出异常来给出提示 | |
if(Objects.isNull(user)){ | |
throw new RuntimeException("用户名或密码错误"); | |
} | |
//TODO 根据用户查询权限信息 添加到 LoginUser 中 | |
// 封装成 UserDetails 对象返回 | |
return new LoginUser(user); | |
} | |
} |
因为 UserDetailsService 方法的返回值是 UserDetails 类型,所以需要定义一个类,实现该接口,把用户信息封装在其中。
@Data | |
@NoArgsConstructor | |
@AllArgsConstructor | |
public class LoginUser implements UserDetails { | |
private User user; | |
@Override | |
public Collection<? extends GrantedAuthority> getAuthorities() { | |
return null; | |
} | |
@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; | |
} | |
} |
在开发中,我们不会使用明文进行加密,默认使用的 PasswordEncoder 要求数据库中的密码格式为:{id} password 。它会根据 id 去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换 PasswordEncoder。
我们一般使用 SpringSecurity 为我们提供的 BCryptPasswordEncoder。
我们只需要使用把 BCryptPasswordEncoder 对象注入 Spring 容器中,SpringSecurity 就会使用该 PasswordEncoder 来进行密码校验。
我们可以定义一个 SpringSecurity 的配置类,SpringSecurity 要求这个配置类要继承 WebSecurityConfigurerAdapter。
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
@Bean | |
public PasswordEncoder passwordEncoder(){ | |
return new BCryptPasswordEncoder(); | |
} | |
} |
# 登录接口
接下我们需要自定义登陆接口,然后让 SpringSecurity 对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
在接口中我们通过 AuthenticationManager 的 authenticate 方法来进行用户认证,所以需要在 SecurityConfig 中配置把 AuthenticationManager 注入容器。
认证成功的话要生成一个 jwt,放入响应中返回。并且为了让用户下回请求时能通过 jwt 识别出具体的是哪个用户,我们需要把用户信息存入 redis,可以把用户 id 作为 key。
@RestController | |
public class LoginController { | |
@Autowired | |
private LoginServcie loginServcie; | |
@PostMapping("/user/login") | |
public ResponseResult login(@RequestBody User user){ | |
return loginServcie.login(user); | |
} | |
} |
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
@Bean | |
public PasswordEncoder passwordEncoder(){ | |
return new BCryptPasswordEncoder(); | |
} | |
@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(); | |
} | |
@Bean | |
@Override | |
public AuthenticationManager authenticationManagerBean() throws Exception { | |
return super.authenticationManagerBean(); | |
} | |
} |
@Service | |
public class LoginServiceImpl implements LoginServcie { | |
@Autowired | |
private AuthenticationManager authenticationManager; | |
@Autowired | |
private 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); | |
} | |
} |
# 认证过滤器
我们需要自定义一个过滤器,这个过滤器会去获取请求头中的 token,对 token 进行解析取出其中的 userid。
使用 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 | |
//TODO 获取权限信息封装到 Authentication 中 | |
UsernamePasswordAuthenticationToken authenticationToken = | |
new UsernamePasswordAuthenticationToken(loginUser,null,null); | |
SecurityContextHolder.getContext().setAuthentication(authenticationToken); | |
// 放行 | |
filterChain.doFilter(request, response); | |
} | |
} |
@Configuration | |
public class SecurityConfig extends WebSecurityConfigurerAdapter { | |
@Bean | |
public PasswordEncoder passwordEncoder(){ | |
return new BCryptPasswordEncoder(); | |
} | |
@Autowired | |
JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; | |
@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); | |
} | |
@Bean | |
@Override | |
public AuthenticationManager authenticationManagerBean() throws Exception { | |
return super.authenticationManagerBean(); | |
} | |
} |
# 退出登录
退出登录查了很多资料,由于 token 不能手动失效,因此退出登录理论上只要前端发送请求不携带 token 就可实现退出登录,但是有些需求仍需要我们手动失效
由于此项目使用了 redis 缓存登录信息,我们只需要定义一个退出登陆接口,然后获取 SecurityContextHolder 中的认证信息,删除 redis 中对应的数据即可,这样在过滤器中无法从 redis 中获取到信息就判断为 token 失效未登录。
@Service | |
public class LoginServiceImpl implements LoginServcie { | |
@Autowired | |
private AuthenticationManager authenticationManager; | |
@Autowired | |
private 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,"退出成功"); | |
} | |
} |
# 授权
例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。
总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。
所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。
# 授权的基本流程
SpringSecurity 为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用
注解去指定访问对应的资源所需的权限。
但是要使用它我们需要先开启相关配置
@EnableGlobalMethodSecurity(prePostEnabled = true) |
然后就可以使用对应的注解。@PreAuthorize
@RestController | |
public class HelloController { | |
@RequestMapping("/hello") | |
@PreAuthorize("hasAuthority('test')") | |
public String hello(){ | |
return "hello"; | |
} | |
} |
# 封装权限信息
我们前面在写 UserDetailsServiceImpl 的时候说过,在查询出用户后还要获取对应的权限信息,封装到 UserDetails 中返回。
我们先直接把权限信息写死封装到 UserDetails 中进行测试。
我们之前定义了 UserDetails 的实现类 LoginUser,想要让其能封装权限信息就要对其进行修改。
此代码主要参考 b 站三更草堂教程编写,文中把 private List<SimpleGrantedAuthority> authorities;
作为一个属性还是非常巧妙的,这样写可以在过滤器中直接 get 到认证信息,用 redis 中间件存储也挺方便的
package com.sangeng.domain; | |
import com.alibaba.fastjson.annotation.JSONField; | |
import lombok.AllArgsConstructor; | |
import lombok.Data; | |
import lombok.NoArgsConstructor; | |
import org.springframework.security.core.GrantedAuthority; | |
import org.springframework.security.core.authority.SimpleGrantedAuthority; | |
import org.springframework.security.core.userdetails.UserDetails; | |
import java.util.Collection; | |
import java.util.List; | |
import java.util.stream.Collectors; | |
@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 所需要的权限信息的集合 | |
private List<SimpleGrantedAuthority> authorities; | |
@Override | |
public Collection<? extends GrantedAuthority> getAuthorities() { | |
if(authorities!=null){ | |
return authorities; | |
} | |
// 把 permissions 中字符串类型的权限信息转换成 GrantedAuthority 对象存入 authorities 中 | |
authorities = permissions.stream(). | |
.map(p -> new SimpleGrantedAuthority(p)) | |
.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; | |
} | |
} |
LoginUser 修改完后我们就可以在 UserDetailsServiceImpl 中去把权限信息封装到 LoginUser 中了。我们写死权限进行测试,后面我们再从数据库中查询权限信息。
package com.sangeng.service.impl; | |
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; | |
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; | |
import com.baomidou.mybatisplus.extension.conditions.query.LambdaQueryChainWrapper; | |
import com.sangeng.domain.LoginUser; | |
import com.sangeng.domain.User; | |
import com.sangeng.mapper.UserMapper; | |
import org.springframework.beans.factory.annotation.Autowired; | |
import org.springframework.security.core.userdetails.UserDetails; | |
import org.springframework.security.core.userdetails.UserDetailsService; | |
import org.springframework.security.core.userdetails.UsernameNotFoundException; | |
import org.springframework.stereotype.Service; | |
import java.util.ArrayList; | |
import java.util.Arrays; | |
import java.util.List; | |
import java.util.Objects; | |
@Service | |
public class UserDetailsServiceImpl implements UserDetailsService { | |
@Autowired | |
private UserMapper userMapper; | |
@Override | |
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); | |
wrapper.eq(User::getUserName,username); | |
User user = userMapper.selectOne(wrapper); | |
if(Objects.isNull(user)){ | |
throw new RuntimeException("用户名或密码错误"); | |
} | |
//TODO 根据用户查询权限信息 添加到 LoginUser 中 | |
List<String> list = new ArrayList<>(Arrays.asList("test")); | |
return new LoginUser(user,list); | |
} | |
} |
# 在过滤器中添加权限信息
把 loginUser 里面的权限信息存入 UsernamePasswordAuthenticationToken 中
LoginUser loginUser = redisCache.getCacheObject("login" + userId); | |
if (Objects.isNull(loginUser)) { | |
throw new RuntimeException("token失效"); | |
} | |
// 存入 SecurityContextHolder | |
//TODO 获取权限信息封装到 Authentication 中 | |
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities()); | |
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); | |
filterChain.doFilter(request, response); |
# 从数据库查询权限信息
RBAC 权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。
# 准备工作
DROP TABLE IF EXISTS `sys_menu`; | |
CREATE TABLE `sys_menu` ( | |
`id` bigint(20) NOT NULL AUTO_INCREMENT, | |
`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名', | |
`path` varchar(200) DEFAULT NULL COMMENT '路由地址', | |
`component` varchar(255) DEFAULT NULL COMMENT '组件路径', | |
`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)', | |
`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)', | |
`perms` varchar(100) DEFAULT NULL COMMENT '权限标识', | |
`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标', | |
`create_by` bigint(20) DEFAULT NULL, | |
`create_time` datetime DEFAULT NULL, | |
`update_by` bigint(20) DEFAULT NULL, | |
`update_time` datetime DEFAULT NULL, | |
`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)', | |
`remark` varchar(500) DEFAULT NULL COMMENT '备注', | |
PRIMARY KEY (`id`) | |
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表'; | |
/*Table structure for table `sys_role` */ | |
DROP TABLE IF EXISTS `sys_role`; | |
CREATE TABLE `sys_role` ( | |
`id` bigint(20) NOT NULL AUTO_INCREMENT, | |
`name` varchar(128) DEFAULT NULL, | |
`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串', | |
`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)', | |
`del_flag` int(1) DEFAULT '0' COMMENT 'del_flag', | |
`create_by` bigint(200) DEFAULT NULL, | |
`create_time` datetime DEFAULT NULL, | |
`update_by` bigint(200) DEFAULT NULL, | |
`update_time` datetime DEFAULT NULL, | |
`remark` varchar(500) DEFAULT NULL COMMENT '备注', | |
PRIMARY KEY (`id`) | |
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表'; | |
/*Table structure for table `sys_role_menu` */ | |
DROP TABLE IF EXISTS `sys_role_menu`; | |
CREATE TABLE `sys_role_menu` ( | |
`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID', | |
`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id', | |
PRIMARY KEY (`role_id`,`menu_id`) | |
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4; | |
/*Table structure for table `sys_user` */ | |
DROP TABLE IF EXISTS `sys_user`; | |
CREATE TABLE `sys_user` ( | |
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键', | |
`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名', | |
`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称', | |
`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码', | |
`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)', | |
`email` varchar(64) DEFAULT NULL COMMENT '邮箱', | |
`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号', | |
`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)', | |
`avatar` varchar(128) DEFAULT NULL COMMENT '头像', | |
`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)', | |
`create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id', | |
`create_time` datetime DEFAULT NULL COMMENT '创建时间', | |
`update_by` bigint(20) DEFAULT NULL COMMENT '更新人', | |
`update_time` datetime DEFAULT NULL COMMENT '更新时间', | |
`del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)', | |
PRIMARY KEY (`id`) | |
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表'; | |
/*Table structure for table `sys_user_role` */ | |
DROP TABLE IF EXISTS `sys_user_role`; | |
CREATE TABLE `sys_user_role` ( | |
`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id', | |
`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id', | |
PRIMARY KEY (`user_id`,`role_id`) | |
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; |
SELECT | |
DISTINCT m.`perms` | |
FROM | |
sys_user_role ur | |
LEFT JOIN `sys_role` r ON ur.`role_id` = r.`id` | |
LEFT JOIN `sys_role_menu` rm ON ur.`role_id` = rm.`role_id` | |
LEFT JOIN `sys_menu` m ON m.`id` = rm.`menu_id` | |
WHERE | |
user_id = 2 | |
AND r.`status` = 0 | |
AND m.`status` = 0 |
然后编写对应的接口,在登录的时候调用,添加权限信息即可
# 自定义异常失败处理
我们还希望在认证失败或者是授权失败的情况下也能和我们的接口一样返回相同结构的 json,这样可以让前端能对响应进行统一的处理。要实现这个功能我们需要知道 SpringSecurity 的异常处理机制。
在 SpringSecurity 中,如果我们在认证或者授权的过程中出现了异常会被 ExceptionTranslationFilter 捕获到。在 ExceptionTranslationFilter 中会去判断是认证失败还是授权失败出现的异常。
如果是认证过程中出现的异常会被封装成 AuthenticationException 然后调用 AuthenticationEntryPoint 对象的方法去进行异常处理。
如果是授权过程中出现的异常会被封装成 AccessDeniedException 然后调用 AccessDeniedHandler 对象的方法去进行异常处理。
所以如果我们需要自定义异常处理,我们只需要自定义 AuthenticationEntryPoint 和 AccessDeniedHandler 然后配置给 SpringSecurity 即可。
- 自定义实现类
@Component | |
public class AccessDeniedHandlerImpl implements AccessDeniedHandler { | |
@Override | |
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { | |
response.setHeader("Access-Control-Allow-Origin", "*"); | |
response.setHeader("Cache-Control","no-cache"); | |
response.setCharacterEncoding("UTF-8"); | |
response.setContentType("application/json"); | |
response.getWriter().println(JSON.toJSONString(CommonResult.forbidden(accessDeniedException.getMessage()))); | |
} | |
} |
@Component | |
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint { | |
@Override | |
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { | |
response.setHeader("Access-Control-Allow-Origin", "*"); | |
response.setHeader("Cache-Control","no-cache"); | |
response.setCharacterEncoding("UTF-8"); | |
response.setContentType("application/json"); | |
response.getWriter().println(JSON.toJSONString(CommonResult.unauthorized(authException.getMessage()))); | |
//flush 的作用是写到 html 里面 | |
response.getWriter().flush(); | |
} | |
} |
- 配置给 SpringSecurity
先注入对应的处理器
@Autowired | |
private AuthenticationEntryPoint authenticationEntryPoint; | |
@Autowired | |
private AccessDeniedHandler accessDeniedHandler; |
然后我们可以使用 HttpSecurity 对象的方法去配置。
http.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint). | |
accessDeniedHandler(accessDeniedHandler); |