22 认证中心介绍

1 概述

  1. 用户身份认证的过程
  2. ruoyi-cloud认证中心的实现没有依赖任何插件,相对简单,一看就懂
  3. 从架构图的角度看认证中心:

    ruoyi-cloud

    1. 登录请求,进到网关
    2. 网关直接调用认证中心。查看ruoyi-gateway-dev.yml:
      
      # 结论:认证服务应用的话,是没有验证码的处理的。
      # 关于验证码的处理,是在网关这边就处理完了。
      spring:
        redis:
          host: localhost
          port: 6379
          password:
        cloud:
          gateway:
            discovery:
              locator:
                lowerCaseServiceId: true
                enabled: true
            routes:
              - id: ruoyi-auth
                uri: lb://ruoyi-auth
                # 第二步:调用后端的认证中心接口  
                predicates:
                  - Path=/auth/**
                filters:
                  # 第一步:校验验证码
                  - CacheRequestFilter
                  - ValidateCodeFilter
                  - StripPrefix=1

2 内容

(1)TokenController.java:登录接口、登出接口、刷新令牌接口、用户注册接口。

(2)LoginBody:用户登录对象,包含username、password这2个属性。

(3)SysLoginService:登录校验方法。服务之间的相互调用,比如在认证模块认证完成后,还要记录日志。

(4)RuoYiAuthApplication:启动类

(5)bootstrap.yml:配置文件 

# Tomcat
server: 
  port: 9200

# Spring
spring: 
  application:
    # 应用名称
    name: ruoyi-auth
  profiles:
    # 环境配置
    active: dev
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: 127.0.0.1:8848
      config:
        # 配置中心地址
        server-addr: 127.0.0.1:8848
        # 配置文件格式
        file-extension: yml
        # 共享配置
        shared-configs:
          - application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}

3 如何使用认证中心去认证?

(1)添加依赖

<!-- ruoyi common security-->
<dependency>
	<groupId>com.ruoyi</groupId>
	<artifactId>ruoyi-common-security</artifactId>
</dependency>

(2)认证启动类

/**
 * 认证授权中心
 */
/**
 * 自定义的ruoyi-security模块提供的注解,
 * 会去加载Feign的注解,
 * 
 */
@EnableRyFeignClients
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class })
public class RuoYiAuthApplication
{
    public static void main(String[] args)
    {
        SpringApplication.run(RuoYiAuthApplication.class, args);
        System.out.println("(♥◠‿◠)ノ゙  认证授权中心启动成功   ლ(´ڡ`ლ)゙  \n" +
                " .-------.       ____     __        \n" +
                " |  _ _   \\      \\   \\   /  /    \n" +
                " | ( ' )  |       \\  _. /  '       \n" +
                " |(_ o _) /        _( )_ .'         \n" +
                " | (_,_).' __  ___(_ o _)'          \n" +
                " |  |\\ \\  |  ||   |(_,_)'         \n" +
                " |  | \\ `'   /|   `-'  /           \n" +
                " |  |  \\    /  \\      /           \n" +
                " ''-'   `'-'    `-..-'              ");
    }
}

 提示:

       目前已经存在ruoyi-auth认证授权中心,用于登录认证,系统退出,刷新令牌。

(3)ruoyi-security#EnableRyFeignClients:自定义的feign注解

/**
 * 自定义feign注解
 * 添加basePackages路径
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@EnableFeignClients
public @interface EnableRyFeignClients
{
    String[] value() default {};

    /**
     * 扫描"com.ruoyi"包及其子孙所下面的所有feign注解,
     * 给它注入进来,不然我们就会实现不了服务之间的相互调用了。
     */
    String[] basePackages() default { "com.ruoyi" };

    Class<?>[] basePackageClasses() default {};

    Class<?>[] defaultConfiguration() default {};

    Class<?>[] clients() default {};
}

23 登录认证实现

1、ruoyi-auth#TokenController#login:登录接口

    @PostMapping("login")
    // LoginBody用户对象,包含了username、password
    public R<?> login(@RequestBody LoginBody form)
    {
        // 用户登录
        LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
        // 获取登录token
        return R.ok(tokenService.createToken(userInfo));
    }

2、ruoyi-auth#SysLoginService#login:登录业务类

    /**
     * 登录
     */
    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已被列入系统黑名单");
        }
        /**
         * openfeign(RPC)实现服务之间的相互调用,查询数据库获取LoginUser对象信息。
         * LoginUser包含:用户令牌、用户的角色集合、用户的权限集合。
          */
        R<LoginUser> userResult = remoteUserService.getUserInfo(username, SecurityConstants.INNER);

        if (StringUtils.isNull(userResult) || StringUtils.isNull(userResult.getData()))
        {
            recordLogService.recordLogininfor(username, Constants.LOGIN_FAIL, "登录用户不存在");
            throw new ServiceException("登录用户:" + username + " 不存在");
        }

        if (R.FAIL == userResult.getCode())
        {
            throw new ServiceException(userResult.getMsg());
        }
        
        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, "登录成功");
        return userInfo;
    }

3、ruoyi-system#SysUserController#info:获取LoginUser对象的信息

    /**
     * 获取当前用户信息
     */
    @InnerAuth
    @GetMapping("/info/{username}")
    public R<LoginUser> info(@PathVariable("username") String username)
    {
        SysUser sysUser = userService.selectUserByUserName(username);
        if (StringUtils.isNull(sysUser))
        {
            return R.fail("用户名或密码错误");
        }
        // 角色集合
        Set<String> roles = permissionService.getRolePermission(sysUser);
        // 权限集合
        Set<String> permissions = permissionService.getMenuPermission(sysUser);
        LoginUser sysUserVo = new LoginUser();
        sysUserVo.setSysUser(sysUser);
        sysUserVo.setRoles(roles);
        sysUserVo.setPermissions(permissions);
        return R.ok(sysUserVo);
    }

4、ruoyi-common-security#TokenService#createToken:生成token

    /**
     * 创建令牌
     */
    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());
        /**
         * redis缓存中数据要刷新一下
         */
        refreshToken(loginUser);

        // Jwt存储信息
        Map<String, Object> claimsMap = new HashMap<String, Object>();
        claimsMap.put(SecurityConstants.USER_KEY, token);
        claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);
        claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);

        // 接口返回信息
        Map<String, Object> rspMap = new HashMap<String, Object>();
        rspMap.put("access_token", JwtUtils.createToken(claimsMap));
        rspMap.put("expires_in", expireTime);
        return rspMap;
    }

5、ruoyi-common-security#TokenService#verifyToken:验证令牌有效期,相差不足120分钟,自动刷新缓存

    /**
     * 验证令牌有效期,相差不足120分钟,自动刷新缓存
     *
     * @param loginUser
     */
    public void verifyToken(LoginUser loginUser)
    {
        long expireTime = loginUser.getExpireTime();
        long currentTime = System.currentTimeMillis();
        if (expireTime - currentTime <= MILLIS_MINUTE_TEN)
        {
            refreshToken(loginUser);
        }
    }

6、ruoyi-common-security#TokenService#refreshToken:刷新令牌有效期

    /**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    public void refreshToken(LoginUser loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        /**
         * 往redis中保存数据
         */
        redisService.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

7、ruoyi-common-core#CacheConstants#LOGIN_TOKEN_KEY:权限缓存前缀

    /**
     * 权限缓存前缀
     */
    public final static String LOGIN_TOKEN_KEY = "login_tokens:";

8、测试

用户登录接口地址 http://localhost:9200/login

请求头Content-Type - application/json,请求方式Post

{
    "username": "admin",
    "password": "admin123"
}

响应结果

{
    "code": 200,
    "data": {
        "access_token": "f840488c-68a9-4272-acc9-c34d3b66a943",
        "expires_in": 43200
    }
}

通过用户验证登录后获取access_token,通过网关访问其他应用数据时必须携带此参数值。

24 刷新令牌实现

1、ruoyi-auth#TokenController#refresh:刷新令牌接口

    @PostMapping("refresh")
    /**
     * request必须包含token
     */
    public R<?> refresh(HttpServletRequest request)
    {
        LoginUser loginUser = tokenService.getLoginUser(request);
        if (StringUtils.isNotNull(loginUser))
        {
            // 刷新令牌有效期
            tokenService.refreshToken(loginUser);
            return R.ok();
        }
        return R.ok();
    }

2、ruoyi-common-security#TokenService#getLoginUser:获取登录用户令牌

/**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginUser getLoginUser(HttpServletRequest request)
    {
        // 获取请求携带的令牌
        String token = SecurityUtils.getToken(request);
        return getLoginUser(token);
    }

    /**
     * 获取用户身份信息
     *
     * @return 用户信息
     */
    public LoginUser getLoginUser(String token)
    {
        LoginUser user = null;
        try
        {
            if (StringUtils.isNotEmpty(token))
            {
                String userkey = JwtUtils.getUserKey(token);
                /**
                 * 从缓存中获取LoginUser
                 */
                user = redisService.getCacheObject(getTokenKey(userkey));
                return user;
            }
        }
        catch (Exception e)
        {
            log.error("获取用户信息异常'{}'", e.getMessage());
        }
        return user;
    }

3、ruoyi-common-security#TokenService#refreshToken:刷新令牌有效期

    /**
     * 刷新令牌有效期
     *
     * @param loginUser 登录信息
     */
    public void refreshToken(LoginUser loginUser)
    {
        loginUser.setLoginTime(System.currentTimeMillis());
        loginUser.setExpireTime(loginUser.getLoginTime() + expireTime * MILLIS_MINUTE);
        // 根据uuid将loginUser缓存
        String userKey = getTokenKey(loginUser.getToken());
        /**
         * 往redis中保存数据
         */
        redisService.setCacheObject(userKey, loginUser, expireTime, TimeUnit.MINUTES);
    }

4、测试postman

顾名思义,就是对系统操作用户的进行缓存刷新,防止过期。

TokenController控制器refresh方法会在用户调用时更新令牌有效期。

刷新令牌接口地址 http://localhost:9200/refresh

请求头Authorization - f840488c-68a9-4272-acc9-c34d3b66a943,请求方式Post

响应结果

{
    "code": 200,
}

刷新后有效期为默720(分钟)。

25 系统退出实现    

顾名思义,就是对系统登用户的退出过程。

TokenController控制器logout方法会在用户退出时删除缓存信息同时保存用户退出日志。源码ruoyi-auth#TokenController.logout:

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

系统退出接口地址 http://localhost:9200/logout

请求头Authorization - f840488c-68a9-4272-acc9-c34d3b66a943,请求方式Delete

{
    "username": "admin",
    "password": "admin123"
}

响应结果

{
    "code": 200,
}

Logo

快速构建 Web 应用程序

更多推荐