JSD-2204-处理登录成功的管理的权限列表-前后端分离-Day13

本文介绍如何在登录成功后处理管理员权限,并将权限信息存入JWT。同时覆盖如何使用Security框架检查权限、自定义UserDetails及通过前端实现登录。

1.处理登录成功的管理的权限列表

目前,存入到Security上下文中的认证信息(Authentication对象)并不包含有效的权限信息(目前是个假信息),为了后续能够判断用户的权限,需要:

  • 当认证(登录)成功后,取出管理员的权限,并将其存入到JWT数据中
  • 后续的请求中的JWT应该已经包含权限,则可以从JWT中解析出权限信息,并存入到认证信息(Authentication对象)中
  • 在操作过程中,应该先将权限列表转换成JSON再存入到JWT中,在解析JWT时,得到的权限信息也是一个JSON数据,需要将其转换成对象才能继续使用

关于JSON格式的转换,有许多工具都可以实现,例如:fastjson

<!-- fastjson:实现对象与JSON的相互转换 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.75</version>
</dependency>

AdminServiceImpl处理登录时,当认证成功时,需要从认证结果中取出权限列表,转换成JSON字符串,并存入到JWT中:

// 原有其它代码
Collection<GrantedAuthority> 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<SimpleGrantedAuthority> authorities = JSON.parseArray(
        authorityListString.toString(), SimpleGrantedAuthority.class);
Authentication authentication = new UsernamePasswordAuthenticationToken(
        username, null, authorities);

// 原有其它代码

完成后,启动项目,正常登录,在服务器端的控制台可以看到相关日志,将显示存入到Security上下文的认证信息中包含权限列表。

1.1使用Security框架检查权限

首先,需要在Security的配置类上开启全局的在方法上检查权限:

// 其它原有注解
@EnableGlobalMethodSecurity(prePostEnabled = true) // 新增
public class SecurityConfiguration ... ...

然后,在控制器类中处理请求的方法上使用@PreAuthorize注解检查权限:

// 其它原有注解
@PreAuthorize("hasAuthority('/ams/admin/update')") // 新增
public JsonResult ...

以上注解表示:必须具有/ams/admin/update权限才允许向此路径提交请求。

提示:Security会根据上下文中的权限列表进行对比,来检查当前登录的用户是否具有此权限。

1.2自定义UserDetails

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);
    }

}

接下来,在UserDetailsServiceImplUserDetails 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<String> permissions = loginInfo.getPermissions(); // /ams/admin/delete
        List<SimpleGrantedAuthority> 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<GrantedAuthority> authorities = loginUser.getAuthorities();
log.debug("认证结果中的权限列表:{}", authorities);
// 【重要】将权限列表转换成JSON格式,用于存储到JWT中
String authorityListString = JSON.toJSONString(authorities);

// 生成JWT
String secretKey = "nmlfdasfdsaurefuifdknjfdskjhajhef";
// 准备Claims
Map<String, Object> 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);

解析得到的idusername都应该封装到认证对象中,进而将认证对象存入到Security上下文中,由于UsernamePasswordAuthenticationToken中的Principal是Object类型的,表示“当事人”,即“当前成功登录的用户”,所以,可以自定义数据类型,封装idusername,并将封装后的对象存入到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<SimpleGrantedAuthority> authorities = JSON.parseArray(
        authorityListString, SimpleGrantedAuthority.class);
Authentication authentication = new UsernamePasswordAuthenticationToken(
        loginPrincipal, null, authorities);

至此,每次客户端携带有效的JWT提交请求时,都可以从中解析得到idusername,这些数据也会保存到Security上下文中,则在任何控制器处理请求的方法上,可以添加@AuthenticationPrincipal LoginPrincipal loginPrincipal,即可注入Security上下文中的LoginPrincipal对象,则可以获取到当事人(当前成功登录的用户)的idusername,例如:

@ApiOperation("查询角色列表")
@ApiOperationSupport(order = 401)
@GetMapping("")
public JsonResult<List<RoleListItemVO>> list(
        @ApiIgnore @AuthenticationPrincipal LoginPrincipal loginPrincipal) {
    log.debug("准备处理【查询角色列表】的请求");
    log.debug("当前登录的用户(当事人)的id:{}", loginPrincipal.getId());
    log.debug("当前登录的用户(当事人)的用户名:{}", loginPrincipal.getUsername());
    List<RoleListItemVO> list = roleService.list();
    return JsonResult.ok(list);
}

提示:以上请求参数还添加了@ApiIgnore注解,其作用是在Knife4j的API文档中忽略此参数,否则,还会在Knife4j文档中显示LoginPrincipal对应的参数。

2.通过前端界面实现登录

{
  "state": 20000,
  "data": "eyJhbGciOiJIUzI1NiIsInR5cCI6Imp3dCJ9.eyJpZCI6MSwiZXhwIjoxNjYxMjM5MzMxLCJhdXRob3JpdGllcyI6Ilt7XCJhdXRob3JpdHlcIjpcIi9hbXMvYWRtaW4vZGVsZXRlXCJ9LHtcImF1dGhvcml0eVwiOlwiL2Ftcy9hZG1pbi9yZWFkXCJ9LHtcImF1dGhvcml0eVwiOlwiL2Ftcy9hZG1pbi91cGRhdGVcIn0se1wiYXV0aG9yaXR5XCI6XCIvcG1zL3Byb2R1Y3QvZGVsZXRlXCJ9LHtcImF1dGhvcml0eVwiOlwiL3Btcy9wcm9kdWN0L3JlYWRcIn0se1wiYXV0aG9yaXR5XCI6XCIvcG1zL3Byb2R1Y3QvdXBkYXRlXCJ9XSIsInVzZXJuYW1lIjoicm9vdCJ9.bLrqPBNVVC9nQejqhGeUhr7QETbVSxoZZaZ-YSK6O6o"
}
{
  "state": 50000,
  "message": "程序运行过程中出现意外错误,请联系系统管理员!"
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值