目前,存入到Security上下文中的认证信息(Authentication对象)并不包含有效的权限信息(目前是个假信息),为了后续能够判断用户的权限,需要:
关于JSON格式的转换,有许多工具都可以实现,例如:fastjson
- <dependency>
- <groupId>com.alibabagroupId>
- <artifactId>fastjsonartifactId>
- <version>1.2.75version>
- dependency>
在AdminServiceImpl处理登录时,当认证成功时,需要从认证结果中取出权限列表,转换成JSON字符串,并存入到JWT中:
- // 原有其它代码
- Collection
authorities = loginUser.getAuthorities(); - log.debug("认证结果中的权限列表:{}", authorities);
- String authorityListString = JSON.toJSONString(authorities); // 【重要】将权限列表转换成JSON格式,用于存储到JWT中
-
- // 生成JWT时的Claims相关代码
- claims.put("authorities", authorityListString);
- log.debug("生成JWT,向JWT中存入authorities:{}", authorityListString);
-
- // 原有其它代码
然后,在JWT过滤器中,当成功的解析JWT时,应该获取权限列表的JSON字符串,并将其转换为认证对象要求的格式(Collection extends GrantedAuthority):
- // 原有其它代码
-
- Object authorityListString = claims.get("authorities");
- log.debug("从JWT中解析得到authorities:{}", authorityListString);
-
- // 准备Authentication对象,后续会将此对象封装到Security的上下文中
- List
authorities = JSON.parseArray( - authorityListString.toString(), SimpleGrantedAuthority.class);
- Authentication authentication = new UsernamePasswordAuthenticationToken(
- username, null, authorities);
-
- // 原有其它代码
完成后,启动项目,正常登录,在服务器端的控制台可以看到相关日志,将显示存入到Security上下文的认证信息中包含权限列表。
首先,需要在Security的配置类上开启全局的在方法上检查权限:
- // 其它原有注解
- @EnableGlobalMethodSecurity(prePostEnabled = true) // 新增
- public class SecurityConfiguration ... ...
然后,在控制器类中处理请求的方法上使用@PreAuthorize注解检查权限:
- // 其它原有注解
- @PreAuthorize("hasAuthority('/ams/admin/update')") // 新增
- public JsonResult ...
以上注解表示:必须具有/ams/admin/update权限才允许向此路径提交请求。
提示:Security会根据上下文中的权限列表进行对比,来检查当前登录的用户是否具有此权限。
Security使用UserDetails接口类型的对象表示需要认证的用户、认证结果中的Principal,但是,Security框架中UserDetails接口的实现类User中并不包含id及其它个性化属性,则可以自定义类进行扩展:
- package cn.tedu.csmall.passport.security;
-
- import lombok.EqualsAndHashCode;
- import lombok.Getter;
- import lombok.Setter;
- import lombok.ToString;
- import org.springframework.security.core.GrantedAuthority;
- import org.springframework.security.core.userdetails.User;
-
- import java.util.Collection;
-
- @Setter
- @Getter
- @EqualsAndHashCode
- @ToString(callSuper = true)
- public class AdminDetails extends User {
-
- /**
- * 管理员id
- */
- private Long id;
-
- public AdminDetails(String username, String password, boolean enabled,
- Collection extends GrantedAuthority> authorities) {
- super(username, password, enabled,
- true, true, true,
- authorities);
- }
-
- }
接下来,在UserDetailsServiceImpl的UserDetails loadUserByUsername(String username)方法的实现中,使用自定义的AdminDetails作为此方法的返回结果类型:
- package cn.tedu.csmall.passport.security;
-
- import cn.tedu.csmall.passport.mapper.AdminMapper;
- import cn.tedu.csmall.passport.pojo.vo.AdminLoginInfoVO;
- import lombok.extern.slf4j.Slf4j;
- import org.springframework.beans.factory.annotation.Autowired;
- import org.springframework.security.core.authority.SimpleGrantedAuthority;
- import org.springframework.security.core.userdetails.User;
- 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.List;
-
- @Slf4j
- @Service
- public class UserDetailsServiceImpl implements UserDetailsService {
-
- @Autowired
- private AdminMapper adminMapper;
-
- @Override
- public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
- log.debug("根据用户名【{}】从数据库查询用户信息……", s);
- // 调用AdminMapper对象,根据用户名(参数值)查询管理员信息
- AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
- // 判断是否查询到有效结果
- if (loginInfo == null) {
- // 根据用户名没有找到任何管理员信息
- String message = "登录失败,用户名不存在!";
- log.warn(message);
- throw new UsernameNotFoundException(message);
- }
-
- log.debug("根据用户名【{}】从数据库查询到有效的用户信息:{}", s, loginInfo);
- // 从查询结果中找出权限信息,转换成Collection extends GrantedAuthority>
- List
permissions = loginInfo.getPermissions(); // /ams/admin/delete - List
authorities = new ArrayList<>(); - for (String permission : permissions) {
- authorities.add(new SimpleGrantedAuthority(permission));
- }
-
- // 返回AdminDetails类型的对象
- AdminDetails adminDetails = new AdminDetails(
- loginInfo.getUsername(), loginInfo.getPassword(),
- loginInfo.getEnable() == 1, authorities);
- adminDetails.setId(loginInfo.getId());
-
- log.debug("即将向Spring Security返回UserDetails:{}", adminDetails);
- return adminDetails;
- }
-
- }
后续,在AdminServiceImpl处理登录时,当认证通过,在认证结果中的Principal就是AdminDetails类型的。
所以,当认证通过后,可以将认证结果中的Principal取出,强制转换为AdminDetails类型,并取出id值,用于生成JWT数据:
- // 原有其它代码
-
- // 处理认证结果
- AdminDetails loginUser = (AdminDetails) authenticateResult.getPrincipal();
- log.debug("认证结果中的管理员id:{}", loginUser.getId());
- log.debug("认证结果中的用户名:{}", loginUser.getUsername());
- Collection
authorities = loginUser.getAuthorities(); - log.debug("认证结果中的权限列表:{}", authorities);
- // 【重要】将权限列表转换成JSON格式,用于存储到JWT中
- String authorityListString = JSON.toJSONString(authorities);
-
- // 生成JWT
- String secretKey = "nmlfdasfdsaurefuifdknjfdskjhajhef";
- // 准备Claims
- Map
claims = new HashMap<>(); - claims.put("id", loginUser.getId());
- claims.put("username", loginUser.getUsername());
- claims.put("authorities", authorityListString);
- log.debug("生成JWT,向JWT中存入id:{}", loginUser.getId());
- log.debug("生成JWT,向JWT中存入username:{}", loginUser.getUsername());
- log.debug("生成JWT,向JWT中存入authorities:{}", authorityListString);
-
- // 原有其它代码
至此,当登录成功后,生成的JWT中将包含id。
接下来,在JWT过滤器中,解析JWT时,就可以解析得到id的值:
- // 尝试解析JWT
- String secretKey = "nmlfdasfdsaurefuifdknjfdskjhajhef";
- Claims claims = Jwts.parser().setSigningKey(secretKey)
- .parseClaimsJws(jwt).getBody();
- Long id = claims.get("id", Long.class);
- String username = claims.get("username", String.class);
- String authorityListString = claims.get("authorities", String.class);
- log.debug("从JWT中解析得到id:{}", id);
- log.debug("从JWT中解析得到username:{}", username);
- log.debug("从JWT中解析得到authorities:{}", authorityListString);
解析得到的id和username都应该封装到认证对象中,进而将认证对象存入到Security上下文中,由于UsernamePasswordAuthenticationToken中的Principal是Object类型的,表示“当事人”,即“当前成功登录的用户”,所以,可以自定义数据类型,封装id和username,并将封装后的对象存入到UsernamePasswordAuthenticationToken中:
- package cn.tedu.csmall.passport.security;
-
- import lombok.Data;
-
- import java.io.Serializable;
-
- /**
- * 用于保存到Security上下文中的、当前登录的管理员信息(不包含权限信息)
- */
- @Data
- public class LoginPrincipal implements Serializable {
-
- /**
- * 当事人id
- */
- private Long id;
- /**
- * 当事人用户名
- */
- private String username;
-
- }
- // 准备Authentication对象,后续会将此对象封装到Security的上下文中
- LoginPrincipal loginPrincipal = new LoginPrincipal();
- loginPrincipal.setId(id);
- loginPrincipal.setUsername(username);
- List
authorities = JSON.parseArray( - authorityListString, SimpleGrantedAuthority.class);
- Authentication authentication = new UsernamePasswordAuthenticationToken(
- loginPrincipal, null, authorities);
至此,每次客户端携带有效的JWT提交请求时,都可以从中解析得到id、username,这些数据也会保存到Security上下文中,则在任何控制器处理请求的方法上,可以添加@AuthenticationPrincipal LoginPrincipal loginPrincipal,即可注入Security上下文中的LoginPrincipal对象,则可以获取到当事人(当前成功登录的用户)的id、username,例如:
- @ApiOperation("查询角色列表")
- @ApiOperationSupport(order = 401)
- @GetMapping("")
- public JsonResult
> list(
- @ApiIgnore @AuthenticationPrincipal LoginPrincipal loginPrincipal) {
- log.debug("准备处理【查询角色列表】的请求");
- log.debug("当前登录的用户(当事人)的id:{}", loginPrincipal.getId());
- log.debug("当前登录的用户(当事人)的用户名:{}", loginPrincipal.getUsername());
- List
list = roleService.list(); - return JsonResult.ok(list);
- }
提示:以上请求参数还添加了@ApiIgnore注解,其作用是在Knife4j的API文档中忽略此参数,否则,还会在Knife4j文档中显示LoginPrincipal对应的参数。
- {
- "state": 20000,
- "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJpZCI6MSwiZXhwIjoxNjYxMjM5MzMxLCJhdXRob3JpdGllcyI6Ilt7XCJhdXRob3JpdHlcIjpcIi9hbXMvYWRtaW4vZGVsZXRlXCJ9LHtcImF1dGhvcml0eVwiOlwiL2Ftcy9hZG1pbi9yZWFkXCJ9LHtcImF1dGhvcml0eVwiOlwiL2Ftcy9hZG1pbi91cGRhdGVcIn0se1wiYXV0aG9yaXR5XCI6XCIvcG1zL3Byb2R1Y3QvZGVsZXRlXCJ9LHtcImF1dGhvcml0eVwiOlwiL3Btcy9wcm9kdWN0L3JlYWRcIn0se1wiYXV0aG9yaXR5XCI6XCIvcG1zL3Byb2R1Y3QvdXBkYXRlXCJ9XSIsInVzZXJuYW1lIjoicm9vdCJ9.bLrqPBNVVC9nQejqhGeUhr7QETbVSxoZZaZ-YSK6O6o"
- }
- {
- "state": 50000,
- "message": "程序运行过程中出现意外错误,请联系系统管理员!"
- }