使用JWT保存权限
在UserDetailsServiceImpl
中,调用的adminMapper.getLoginInfoByUsername()
中已经包含用户的权限,则,在返回的UserDetails
对象中封装权限信息:
UserDetails userDetails = User.builder()
.username(loginAdmin.getUsername())
.password(loginAdmin.getPassword())
.accountExpired(false)
.accountLocked(false)
.credentialsExpired(false)
.disabled(loginAdmin.getEnable() == 0)
.authorities(loginAdmin.getPermissions().toArray(new String[] {})) // 调整
.build();
在AdminServiceImpl
中,执行认证且成功后,返回的Authentication
对象中的“当事人”就是以上返回的UserDetails
对象,所以,此对象中是包含了以上封装的权限信息的,则可以将权限信息取出并封装到JWT中。
需要注意:如果直接将权限(Collection<? extends GrantedAuthority>
)存入到JWT数据中,相当于把Collection<? extends GrantedAuthority>
转换成String
,此过程会丢失数据的原始类型,且不符合自动反序列化格式,后续解析时,无法直接还原成Collection<? extends GrantedAuthority>
类型!为解决此问题,可以先将Collection<? extends GrantedAuthority>
转换成JSON格式的字符串再存入到JWT中,后续,解析JWT时得到的也会是JSON格式的字符串,可以反序列化为Collection<? extends GrantedAuthority>
格式!
则先添加JSON工具类的依赖项:
<!-- fastjson:实现对象与JSON的相互转换 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.75</version>
</dependency>
然后,在AdminServiceImpl
中,先从认证成功的返回结果中取出权限,然后存入到JWT中:
// 从认证返回结果中取出当事人信息
User principal = (User) authenticateResult.getPrincipal();
String username = principal.getUsername();
log.debug("认证信息中的用户名:{}", username);
// ===== 以下是新增 ======
Collection<GrantedAuthority> authorities = principal.getAuthorities();
log.debug("认证信息中的权限:{}", authorities);
String authorityListString = JSON.toJSONString(authorities);
log.debug("认证信息中的权限转换为JSON字符串:{}", authorityListString);
// 生成JWT,并返回
// 准备Claims值
Map<String, Object> claims = new HashMap<>();
claims.put("username", username);
claims.put("authorities", authorityListString); // 新增
最后,在JwtAuthorizationFilter
中,解析JWT时,取出权限的JSON字符串,将其反序列化为符合Collection<? extends GrantedAuthority>
的格式:List<SimpleGrantedAuthority>
,并用于存入到认证信息中:
String username = claims.get("username", String.class);
log.debug("从JWT中解析得到【username】的值:{}", username);
String authorityListString = claims.get("authorities", String.class); // 新增
log.debug("从JWT中解析得到【authorities】的值:{}", authorityListString); // 新增
// 准备权限,将封装到认证信息中
List<SimpleGrantedAuthority> authorityList
= JSON.parseArray(authorityListString, SimpleGrantedAuthority.class);
// 准备存入到SecurityContext的认证信息
Authentication authentication
= new UsernamePasswordAuthenticationToken(
username, null, authorityList);
使用Spring Security控制访问权限
首先,需要在Spring Security的配置类上开启方法前的权限检查:
@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // 新增
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
// 省略配置类中原有代码
}
然后,在需要对权限进行检查(控制)的控制器类的方法上,使用注解来配置权限,例如:
// http://localhost:9081/admins
@ApiOperation("查询管理员列表")
@ApiOperationSupport(order = 400)
@PreAuthorize("hasAuthority('/ams/admin/read')") // 新增
@GetMapping("")
public JsonResult<List<AdminListItemVO>> list() {
log.debug("开始处理【查询管理员列表】的请求……");
List<AdminListItemVO> list = adminService.list();
return JsonResult.ok(list);
}
以上新增的@PreAuthorize("hasAuthority('/ams/admin/read')")
就表示已经通过认证的用户必须具有 '/ams/admin/read'
权限才可以访问此请求路径(http://localhost:9081/admins`),如果没有权限,将抛出`org.springframework.security.access.AccessDeniedException: 不允许访问
。
由于无操作权限时会出现新的异常,则在GlobalExceptionHandler
中补充对此类异常的处理:
@ExceptionHandler
public JsonResult<Void> handleAccessDeniedException(AccessDeniedException e) {
log.debug("处理AccessDeniedException");
Integer serviceCode = ServiceCode.ERR_FORBIDDEN.getValue();
String message = "请求失败,当前账号无此操作权限!";
return JsonResult.fail(serviceCode, message);
}