第05章 - 认证与授权中心

第05章 - 认证与授权中心

5.1 认证中心概述

5.1.1 认证中心职责

ruoyi-auth认证中心是RuoYi-Cloud的核心安全组件,负责处理系统的身份认证和Token管理:

┌─────────────────────────────────────────────────────────────────────────────┐
│                            认证中心 (ruoyi-auth)                             │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                          核心功能                                   │    │
│  ├─────────────────┬─────────────────┬─────────────────┬──────────────┤    │
│  │   用户登录      │   用户登出      │   用户注册      │  Token刷新   │    │
│  └─────────────────┴─────────────────┴─────────────────┴──────────────┘    │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                          安全机制                                   │    │
│  ├─────────────────┬─────────────────┬─────────────────┬──────────────┤    │
│  │  密码加密验证   │   验证码校验    │   登录限制      │  日志记录    │    │
│  └─────────────────┴─────────────────┴─────────────────┴──────────────┘    │
│                                                                             │
│  ┌─────────────────────────────────────────────────────────────────────┐    │
│  │                          Token管理                                  │    │
│  ├─────────────────┬─────────────────┬─────────────────┬──────────────┤    │
│  │  Token生成      │   Token存储     │   Token验证     │  Token续期   │    │
│  └─────────────────┴─────────────────┴─────────────────┴──────────────┘    │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

5.1.2 项目结构

ruoyi-auth/
├── src/main/java/com/ruoyi/auth/
│   ├── RuoYiAuthApplication.java    # 启动类
│   ├── controller/
│   │   └── TokenController.java     # 认证控制器
│   ├── form/
│   │   ├── LoginBody.java           # 登录请求体
│   │   └── RegisterBody.java        # 注册请求体
│   └── service/
│       ├── SysLoginService.java     # 登录服务
│       ├── SysPasswordService.java  # 密码服务
│       ├── SysRecordLogService.java # 日志服务
│       └── TokenService.java        # Token服务
└── src/main/resources/
    └── bootstrap.yml                # 启动配置

5.2 登录认证流程

5.2.1 完整登录流程

┌────────────┐     ┌────────────┐     ┌────────────┐     ┌────────────┐     ┌────────────┐
│   Client   │     │  Gateway   │     │    Auth    │     │   System   │     │   Redis    │
└─────┬──────┘     └─────┬──────┘     └─────┬──────┘     └─────┬──────┘     └─────┬──────┘
      │                  │                  │                  │                  │
      │ 1.登录请求        │                  │                  │                  │
      │─────────────────>│                  │                  │                  │
      │                  │ 2.验证码校验      │                  │                  │
      │                  │─────────────────>│                  │                  │
      │                  │                  │ 3.获取验证码       │                  │
      │                  │                  │─────────────────────────────────────>│
      │                  │                  │ 4.返回验证码       │                  │
      │                  │                  │<─────────────────────────────────────│
      │                  │ 5.校验结果        │                  │                  │
      │                  │<─────────────────│                  │                  │
      │                  │                  │                  │                  │
      │                  │ 6.转发认证请求    │                  │                  │
      │                  │─────────────────>│                  │                  │
      │                  │                  │ 7.查询用户         │                  │
      │                  │                  │─────────────────>│                  │
      │                  │                  │ 8.返回用户信息     │                  │
      │                  │                  │<─────────────────│                  │
      │                  │                  │                  │                  │
      │                  │                  │ 9.验证密码        │                  │
      │                  │                  │───────┐          │                  │
      │                  │                  │       │          │                  │
      │                  │                  │<──────┘          │                  │
      │                  │                  │                  │                  │
      │                  │                  │ 10.生成Token      │                  │
      │                  │                  │───────┐          │                  │
      │                  │                  │       │          │                  │
      │                  │                  │<──────┘          │                  │
      │                  │                  │                  │                  │
      │                  │                  │ 11.存储Token      │                  │
      │                  │                  │─────────────────────────────────────>│
      │                  │                  │ 12.存储成功       │                  │
      │                  │                  │<─────────────────────────────────────│
      │                  │                  │                  │                  │
      │                  │ 13.返回Token     │                  │                  │
      │                  │<─────────────────│                  │                  │
      │ 14.返回登录结果   │                  │                  │                  │
      │<─────────────────│                  │                  │                  │
      │                  │                  │                  │                  │

5.2.2 登录控制器

@RestController
public class TokenController {
    
    @Autowired
    private TokenService tokenService;
    
    @Autowired
    private SysLoginService sysLoginService;
    
    /**
     * 登录方法
     */
    @PostMapping("login")
    public R<?> login(@RequestBody LoginBody form) {
        // 用户登录
        LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
        // 获取登录token
        return R.ok(tokenService.createToken(userInfo));
    }
    
    /**
     * 登出方法
     */
    @DeleteMapping("logout")
    public R<?> logout(HttpServletRequest request) {
        String token = SecurityUtils.getToken(request);
        if (StringUtils.isNotEmpty(token)) {
            String username = JwtUtils.getUserName(token);
            // 删除用户缓存记录
            AuthUtil.logoutByToken(token);
            // 记录用户退出日志
            sysLoginService.logout(username);
        }
        return R.ok();
    }
    
    /**
     * 刷新方法
     */
    @PostMapping("refresh")
    public R<?> refresh(HttpServletRequest request) {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (loginUser != null) {
            // 刷新令牌有效期
            tokenService.refreshToken(loginUser);
            return R.ok();
        }
        return R.ok();
    }
    
    /**
     * 注册方法
     */
    @PostMapping("register")
    public R<?> register(@RequestBody RegisterBody registerBody) {
        // 用户注册
        sysLoginService.register(registerBody.getUsername(), registerBody.getPassword());
        return R.ok();
    }
}

5.2.3 登录服务实现

@Component
public class SysLoginService {
    
    @Autowired
    private RemoteUserService remoteUserService;
    
    @Autowired
    private SysPasswordService passwordService;
    
    @Autowired
    private SysRecordLogService recordLogService;
    
    /**
     * 登录
     */
    public LoginUser login(String username, String password) {
        // 用户名或密码为空 错误
        if (StringUtils.isAnyBlank(username, password)) {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户/密码必须填写");
            throw new ServiceException("用户/密码必须填写");
        }
        
        // 密码如果不在指定范围内 错误
        if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
                || password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户密码不在指定范围");
            throw new ServiceException("用户密码不在指定范围");
        }
        
        // 用户名不在指定范围内 错误
        if (username.length() < UserConstants.USERNAME_MIN_LENGTH
                || username.length() > UserConstants.USERNAME_MAX_LENGTH) {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户名不在指定范围");
            throw new ServiceException("用户名不在指定范围");
        }
        
        // IP黑名单校验
        String blackStr = Convert.toStr(redisService.getCacheObject(CacheConstants.SYS_LOGIN_BLACKIPLIST));
        if (IpUtils.isMatchedIp(blackStr, IpUtils.getIpAddr())) {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "很抱歉,访问IP已被列入系统黑名单");
            throw new ServiceException("很抱歉,访问IP已被列入系统黑名单");
        }
        
        // 查询用户信息
        R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER);
        
        if (R.FAIL == userResult.getCode()) {
            throw new ServiceException(userResult.getMsg());
        }
        
        if (userResult.getData() == null) {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "登录用户不存在");
            throw new ServiceException("登录用户:" + username + " 不存在");
        }
        
        LoginUser userInfo = userResult.getData();
        SysUser user = userResult.getData().getSysUser();
        
        if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "对不起,您的账号已被删除");
            throw new ServiceException("对不起,您的账号:" + username + " 已被删除");
        }
        
        if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "用户已停用,请联系管理员");
            throw new ServiceException("对不起,您的账号:" + username + " 已停用");
        }
        
        // 密码校验
        passwordService.validate(user, password);
        
        // 记录登录信息
        recordLogService.recordLogininfor(username, Constants.LOGIN_SUCCESS, "登录成功");
        recordLoginInfo(user.getUserId());
        
        return userInfo;
    }
    
    /**
     * 记录登录信息
     */
    public void recordLoginInfo(Long userId) {
        SysUser sysUser = new SysUser();
        sysUser.setUserId(userId);
        sysUser.setLoginIp(IpUtils.getIpAddr());
        sysUser.setLoginDate(DateUtils.getNowDate());
        remoteUserService.recordUserLogin(sysUser, SecurityConstants.INNER);
    }
    
    /**
     * 注销登录
     */
    public void logout(String loginName) {
        recordLogService.recordLogininfor(loginName, Constants.LOGOUT, "退出成功");
    }
    
    /**
     * 注册
     */
    public void register(String username, String password) {
        // 用户名或密码为空 错误
        if (StringUtils.isAnyBlank(username, password)) {
            throw new ServiceException("用户/密码必须填写");
        }
        
        if (username.length() < UserConstants.USERNAME_MIN_LENGTH
                || username.length() > UserConstants.USERNAME_MAX_LENGTH) {
            throw new ServiceException("账户长度必须在2到20个字符之间");
        }
        
        if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
                || password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
            throw new ServiceException("密码长度必须在5到20个字符之间");
        }
        
        // 注册用户信息
        SysUser sysUser = new SysUser();
        sysUser.setUserName(username);
        sysUser.setNickName(username);
        sysUser.setPassword(SecurityUtils.encryptPassword(password));
        R<?> registerResult = remoteUserService.registerUserInfo(sysUser, SecurityConstants.INNER);
        
        if (R.FAIL == registerResult.getCode()) {
            throw new ServiceException(registerResult.getMsg());
        }
        
        recordLogService.recordLogininfor(username, Constants.REGISTER, "注册成功");
    }
}

5.3 密码安全

5.3.1 密码加密

RuoYi-Cloud使用BCrypt算法对密码进行加密:

public class SecurityUtils {
    
    /**
     * 生成BCryptPasswordEncoder密码
     */
    public static String encryptPassword(String password) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.encode(password);
    }
    
    /**
     * 判断密码是否相同
     */
    public static boolean matchesPassword(String rawPassword, String encodedPassword) {
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
        return passwordEncoder.matches(rawPassword, encodedPassword);
    }
}

5.3.2 密码验证服务

@Component
public class SysPasswordService {
    
    @Autowired
    private RedisService redisService;
    
    @Value(value = "${user.password.maxRetryCount}")
    private int maxRetryCount;
    
    @Value(value = "${user.password.lockTime}")
    private int lockTime;
    
    /**
     * 登录账户密码错误次数缓存键名
     */
    private String getCacheKey(String username) {
        return CacheConstants.PWD_ERR_CNT_KEY + username;
    }
    
    /**
     * 验证密码
     */
    public void validate(SysUser user, String password) {
        String username = user.getUserName();
        
        Integer retryCount = redisService.getCacheObject(getCacheKey(username));
        
        if (retryCount == null) {
            retryCount = 0;
        }
        
        // 验证是否超过最大重试次数
        if (retryCount >= Integer.valueOf(maxRetryCount).intValue()) {
            String errMsg = StringUtils.format("密码输入错误{}次,帐户锁定{}分钟", maxRetryCount, lockTime);
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, errMsg);
            throw new ServiceException(errMsg);
        }
        
        // 密码匹配校验
        if (!SecurityUtils.matchesPassword(password, user.getPassword())) {
            retryCount = retryCount + 1;
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, 
                StringUtils.format("密码输入错误{}次", retryCount));
            redisService.setCacheObject(getCacheKey(username), retryCount, lockTime, TimeUnit.MINUTES);
            throw new ServiceException("用户不存在/密码错误");
        } else {
            clearLoginRecordCache(username);
        }
    }
    
    /**
     * 清除登录记录缓存
     */
    public void clearLoginRecordCache(String loginName) {
        if (redisService.hasKey(getCacheKey(loginName))) {
            redisService.deleteObject(getCacheKey(loginName));
        }
    }
}

5.4 Token管理

5.4.1 Token服务实现

@Component
public class TokenService {
    
    @Autowired
    private RedisService redisService;
    
    /** 令牌自定义标识 */
    @Value("${token.header}")
    private String header;
    
    /** 令牌秘钥 */
    @Value("${token.secret}")
    private String secret;
    
    /** 令牌有效期(默认30分钟) */
    @Value("${token.expireTime}")
    private int expireTime;
    
    protected static final long MILLIS_SECOND = 1000;
    protected static final long MILLIS_MINUTE = 60 * MILLIS_SECOND;
    private final static long MILLIS_MINUTE_TEN = 20 * 60 * 1000L;
    
    /**
     * 创建令牌
     */
    public Map<String, Object> createToken(LoginUser loginUser) {
        String token = IdUtils.fastUUID();
        Long userId = loginUser.getSysUser().getUserId();
        String userName = loginUser.getSysUser().getUserName();
        
        loginUser.setToken(token);
        loginUser.setUserid(userId);
        loginUser.setUsername(userName);
        loginUser.setIpaddr(IpUtils.getIpAddr());
        
        refreshToken(loginUser);
        
        // 生成JWT
        Map<String, Object> claims = new HashMap<>();
        claims.put(SecurityConstants.USER_KEY, token);
        claims.put(SecurityConstants.DETAILS_USER_ID, userId);
        claims.put(SecurityConstants.DETAILS_USERNAME, userName);
        
        Map<String, Object> rspMap = new HashMap<>();
        rspMap.put("access_token", JwtUtils.createToken(claims));
        rspMap.put("expires_in", expireTime);
        return rspMap;
    }
    
    /**
     * 获取用户身份信息
     */
    public LoginUser getLoginUser(HttpServletRequest request) {
        // 获取请求携带的令牌
        String token = getToken(request);
        return getLoginUser(token);
    }
    
    /**
     * 获取用户身份信息
     */
    public LoginUser getLoginUser(String token) {
        LoginUser user = null;
        try {
            if (StringUtils.isNotEmpty(token)) {
                String userKey = JwtUtils.getUserKey(token);
                user = redisService.getCacheObject(getTokenKey(userKey));
            }
        } catch (Exception e) {
            log.error("获取用户信息异常'{}'", e.getMessage());
        }
        return user;
    }
    
    /**
     * 设置用户身份信息
     */
    public void setLoginUser(LoginUser loginUser) {
        if (loginUser != null && StringUtils.isNotEmpty(loginUser.getToken())) {
            refreshToken(loginUser);
        }
    }
    
    /**
     * 删除用户缓存信息
     */
    public void delLoginUser(String token) {
        if (StringUtils.isNotEmpty(token)) {
            String userKey = JwtUtils.getUserKey(token);
            redisService.deleteObject(getTokenKey(userKey));
        }
    }
    
    /**
     * 刷新令牌有效期
     */
    public void refreshToken(LoginUser loginUser) {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        redisService.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }
    
    /**
     * 验证令牌有效期,相差不足20分钟,自动刷新缓存
     */
    public void verifyToken(LoginUser loginUser) {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN) {
            refreshToken(loginUser);
        }
    }
    
    /**
     * 获取请求token
     */
    private String getToken(HttpServletRequest request) {
        String token = request.getHeader(header);
        if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) {
            token = token.replace(TokenConstants.PREFIX, "");
        }
        return token;
    }
    
    private String getTokenKey(String token) {
        return CacheConstants.LOGIN_TOKEN_KEY + token;
    }
}

5.4.2 JWT工具类

public class JwtUtils {
    
    public static String secret = TokenConstants.SECRET;
    
    /**
     * 从数据声明生成令牌
     */
    public static String createToken(Map<String, Object> claims) {
        String token = Jwts.builder()
                .setClaims(claims)
                .signWith(SignatureAlgorithm.HS512, secret)
                .compact();
        return token;
    }
    
    /**
     * 从令牌中获取数据声明
     */
    public static Claims parseToken(String token) {
        return Jwts.parser()
                .setSigningKey(secret)
                .parseClaimsJws(token)
                .getBody();
    }
    
    /**
     * 根据令牌获取用户标识
     */
    public static String getUserKey(String token) {
        Claims claims = parseToken(token);
        return getValue(claims, SecurityConstants.USER_KEY);
    }
    
    /**
     * 根据令牌获取用户ID
     */
    public static String getUserId(String token) {
        Claims claims = parseToken(token);
        return getValue(claims, SecurityConstants.DETAILS_USER_ID);
    }
    
    /**
     * 根据令牌获取用户名
     */
    public static String getUserName(String token) {
        Claims claims = parseToken(token);
        return getValue(claims, SecurityConstants.DETAILS_USERNAME);
    }
    
    /**
     * 根据身份信息获取键值
     */
    public static String getValue(Claims claims, String key) {
        return Convert.toStr(claims.get(key), "");
    }
}

5.5 验证码机制

5.5.1 验证码配置

# 安全配置
security:
  # 验证码
  captcha:
    enabled: true          # 是否开启验证码
    type: math             # 验证码类型:math-数学计算, char-字符

5.5.2 验证码生成

@RestController
public class CaptchaController {
    
    @Autowired
    private Producer captchaProducer;
    
    @Autowired
    private Producer captchaProducerMath;
    
    @Autowired
    private RedisService redisService;
    
    @Value("${security.captcha.enabled}")
    private Boolean captchaEnabled;
    
    @Value("${security.captcha.type}")
    private String captchaType;
    
    /**
     * 生成验证码
     */
    @GetMapping("/captchaImage")
    public AjaxResult getCode(HttpServletResponse response) {
        AjaxResult ajax = AjaxResult.success();
        ajax.put("captchaEnabled", captchaEnabled);
        if (!captchaEnabled) {
            return ajax;
        }
        
        // 保存验证码信息
        String uuid = IdUtils.simpleUUID();
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
        
        String capStr = null, code = null;
        BufferedImage image = null;
        
        if ("math".equals(captchaType)) {
            String capText = captchaProducerMath.createText();
            capStr = capText.substring(0, capText.lastIndexOf("@"));
            code = capText.substring(capText.lastIndexOf("@") + 1);
            image = captchaProducerMath.createImage(capStr);
        } else if ("char".equals(captchaType)) {
            capStr = code = captchaProducer.createText();
            image = captchaProducer.createImage(capStr);
        }
        
        redisService.setCacheObject(verifyKey, code, Constants.CAPTCHA_EXPIRATION, TimeUnit.MINUTES);
        
        // 转换流信息写出
        FastByteArrayOutputStream os = new FastByteArrayOutputStream();
        try {
            ImageIO.write(image, "jpg", os);
        } catch (IOException e) {
            return AjaxResult.error(e.getMessage());
        }
        
        ajax.put("uuid", uuid);
        ajax.put("img", Base64.encode(os.toByteArray()));
        return ajax;
    }
}

5.5.3 验证码过滤器

@Component
public class ValidateCodeFilter extends AbstractGatewayFilterFactory<Object> {
    
    @Autowired
    private ValidateCodeService validateCodeService;
    
    @Autowired
    private CaptchaProperties captchaProperties;
    
    private static final String[] VALIDATE_URL = new String[] { "/auth/login", "/auth/register" };
    
    @Override
    public GatewayFilter apply(Object config) {
        return (exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();
            
            // 非登录/注册请求或验证码关闭,不处理
            if (!StringUtils.equalsAnyIgnoreCase(request.getURI().getPath(), VALIDATE_URL) 
                    || !captchaProperties.getEnabled()) {
                return chain.filter(exchange);
            }
            
            try {
                String rspStr = resolveBodyFromRequest(request);
                JSONObject obj = JSON.parseObject(rspStr);
                validateCodeService.checkCaptcha(obj.getString("code"), obj.getString("uuid"));
            } catch (Exception e) {
                return ServletUtils.webFluxResponseWriter(exchange.getResponse(), e.getMessage());
            }
            return chain.filter(exchange);
        };
    }
}

5.5.4 验证码校验

@Service
public class ValidateCodeServiceImpl implements ValidateCodeService {
    
    @Autowired
    private RedisService redisService;
    
    /**
     * 校验验证码
     */
    @Override
    public void checkCaptcha(String code, String uuid) throws CaptchaException {
        if (StringUtils.isEmpty(code)) {
            throw new CaptchaException("验证码不能为空");
        }
        if (StringUtils.isEmpty(uuid)) {
            throw new CaptchaException("验证码已失效");
        }
        
        String verifyKey = CacheConstants.CAPTCHA_CODE_KEY + uuid;
        String captcha = redisService.getCacheObject(verifyKey);
        redisService.deleteObject(verifyKey);
        
        if (captcha == null) {
            throw new CaptchaException("验证码已失效");
        }
        if (!code.equalsIgnoreCase(captcha)) {
            throw new CaptchaException("验证码错误");
        }
    }
}

5.6 登录日志记录

5.6.1 日志记录服务

@Component
public class SysRecordLogService {
    
    @Autowired
    private RemoteLogService remoteLogService;
    
    /**
     * 记录登录信息
     */
    public void recordLogininfor(String username, String status, String message) {
        SysLogininfor logininfor = new SysLogininfor();
        logininfor.setUserName(username);
        logininfor.setIpaddr(IpUtils.getIpAddr());
        logininfor.setMsg(message);
        
        // 日志状态
        if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER)) {
            logininfor.setStatus(Constants.LOGIN_SUCCESS_STATUS);
        } else if (Constants.LOGIN_FAIL.equals(status)) {
            logininfor.setStatus(Constants.LOGIN_FAIL_STATUS);
        }
        
        remoteLogService.saveLogininfor(logininfor, SecurityConstants.INNER);
    }
}

5.6.2 登录日志实体

public class SysLogininfor extends BaseEntity {
    
    /** ID */
    private Long infoId;
    
    /** 用户账号 */
    private String userName;
    
    /** 状态 0成功 1失败 */
    private String status;
    
    /** 地址 */
    private String ipaddr;
    
    /** 描述 */
    private String msg;
    
    /** 访问时间 */
    private Date accessTime;
    
    // getter/setter...
}

5.7 权限模型

5.7.1 RBAC权限模型

RuoYi-Cloud采用RBAC(Role-Based Access Control)基于角色的访问控制模型:

┌─────────────────────────────────────────────────────────────────────────────┐
│                            RBAC 权限模型                                    │
├─────────────────────────────────────────────────────────────────────────────┤
│                                                                             │
│  ┌─────────────┐                                                            │
│  │    用户     │──────────────┬──────────────┬──────────────┐               │
│  │   (User)    │              │              │              │               │
│  └─────────────┘              │              │              │               │
│                               ▼              ▼              ▼               │
│                    ┌─────────────┐ ┌─────────────┐ ┌─────────────┐          │
│                    │   角色1     │ │   角色2     │ │   角色3     │          │
│                    │   (Role)    │ │   (Role)    │ │   (Role)    │          │
│                    └──────┬──────┘ └──────┬──────┘ └──────┬──────┘          │
│                           │              │              │                   │
│              ┌────────────┴────────────┐ │              │                   │
│              │                         │ │              │                   │
│              ▼                         ▼ ▼              ▼                   │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │   菜单1     │ │   菜单2     │ │   菜单3     │ │   菜单4     │           │
│  │   (Menu)    │ │   (Menu)    │ │   (Menu)    │ │   (Menu)    │           │
│  └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘           │
│         │              │              │              │                      │
│         ▼              ▼              ▼              ▼                      │
│  ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐           │
│  │   按钮1     │ │   按钮2     │ │   按钮3     │ │   按钮4     │           │
│  │(Permission) │ │(Permission) │ │(Permission) │ │(Permission) │           │
│  └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘           │
│                                                                             │
└─────────────────────────────────────────────────────────────────────────────┘

5.7.2 用户权限加载

@Service
public class SysPermissionService {
    
    @Autowired
    private ISysRoleService roleService;
    
    @Autowired
    private ISysMenuService menuService;
    
    /**
     * 获取角色数据权限
     */
    public Set<String> getRolePermission(SysUser user) {
        Set<String> roles = new HashSet<String>();
        // 管理员拥有所有权限
        if (user.isAdmin()) {
            roles.add("admin");
        } else {
            roles.addAll(roleService.selectRolePermissionByUserId(user.getUserId()));
        }
        return roles;
    }
    
    /**
     * 获取菜单数据权限
     */
    public Set<String> getMenuPermission(SysUser user) {
        Set<String> perms = new HashSet<String>();
        // 管理员拥有所有权限
        if (user.isAdmin()) {
            perms.add("*:*:*");
        } else {
            List<SysRole> roles = user.getRoles();
            if (!CollectionUtils.isEmpty(roles)) {
                // 多角色设置permissions属性,以便数据权限匹配权限
                for (SysRole role : roles) {
                    Set<String> rolePerms = menuService.selectMenuPermsByRoleId(role.getRoleId());
                    role.setPermissions(rolePerms);
                    perms.addAll(rolePerms);
                }
            } else {
                perms.addAll(menuService.selectMenuPermsByUserId(user.getUserId()));
            }
        }
        return perms;
    }
}

5.7.3 LoginUser结构

public class LoginUser implements Serializable {
    
    /** 用户唯一标识 */
    private String token;
    
    /** 用户ID */
    private Long userid;
    
    /** 用户名 */
    private String username;
    
    /** 登录时间 */
    private Long loginTime;
    
    /** 过期时间 */
    private Long expireTime;
    
    /** 登录IP地址 */
    private String ipaddr;
    
    /** 权限列表 */
    private Set<String> permissions;
    
    /** 角色列表 */
    private Set<String> roles;
    
    /** 用户信息 */
    private SysUser sysUser;
    
    /**
     * 是否具有指定权限
     */
    public boolean hasPermission(String permission) {
        if (StringUtils.isEmpty(permission)) {
            return false;
        }
        if (permissions == null || permissions.isEmpty()) {
            return false;
        }
        for (String perm : permissions) {
            if (ALL_PERMISSION.equals(perm) || perm.equals(permission)) {
                return true;
            }
        }
        return false;
    }
    
    /**
     * 是否具有指定角色
     */
    public boolean hasRole(String role) {
        if (StringUtils.isEmpty(role)) {
            return false;
        }
        if (roles == null || roles.isEmpty()) {
            return false;
        }
        for (String r : roles) {
            if (SUPER_ADMIN.equals(r) || r.equals(role)) {
                return true;
            }
        }
        return false;
    }
}

5.8 安全配置

5.8.1 Nacos配置

# application-dev.yml
security:
  # 验证码
  captcha:
    enabled: true
    type: math
  # XSS过滤
  xss:
    enabled: true
    excludeUrls:
      - /system/notice
  # 不校验白名单
  ignore:
    whites:
      - /auth/logout
      - /auth/login
      - /auth/register
      - /*/v3/api-docs
      - /csrf

5.8.2 Token配置

# Token配置
token:
  # 令牌自定义标识
  header: Authorization
  # 令牌密钥
  secret: abcdefghijklmnopqrstuvwxyz
  # 令牌有效期(默认30分钟)
  expireTime: 30

5.9 小结

本章详细介绍了RuoYi-Cloud的认证与授权中心,包括:

  1. 认证中心职责:Token管理、用户认证、安全机制
  2. 登录流程:完整的登录认证流程
  3. 密码安全:BCrypt加密、密码验证、登录限制
  4. Token管理:创建、存储、验证、刷新
  5. 验证码:生成、校验机制
  6. 日志记录:登录日志记录
  7. 权限模型:RBAC权限模型

理解认证授权机制是进行安全开发的基础,下一章我们将深入了解网关服务。


上一章:微服务模块详解 | 返回目录 | 下一章:网关服务详解

posted @ 2026-01-08 14:05  我才是银古  阅读(4)  评论(0)    收藏  举报