【若依SpringCloud】学习笔记二(ruoyiCloud token生成及验证)

权限控制表数据库设计

一、登录

1、Nacos配置(ruoyi-gateway-dev.yml。 匹配请求路径为/auth/的请求,设置过滤器并指定通过过滤器后的转发到ruoyi-auth服务并根据请求uri 匹配ruoyi-auth TokenController处理其中的处理方法

      routes:
        # 认证中心
        # 指定该路由规则的ID为ruoyi-auth
        - id: ruoyi-auth
          #指定请求转发的目标服务为ruoyi-auth,采用负载均衡的方式进行转发。lb代表从服务注册发现组件(如Eureka、Consul、Nacos等)中获取服务列表
          uri: lb://ruoyi-auth
          #匹配请求路径为/auth/**的请求
          predicates:
            - Path=/auth/**
          filters:
            # 验证码处理
            # 缓存请求的过滤器
            - CacheRequestFilter
            # 验证码处理的过滤器
            - ValidateCodeFilter
            # 去掉请求路径中的前缀,这里设置为1表示去掉第一个路径元素
            - StripPrefix=1

2、后端代码

(1) TokenController(控制层。调用业务层sysLoginService并从登录上送表单中获取用户名和密码用来执行后续登录服务)
    @PostMapping("login")
    public R<?> login(@RequestBody LoginBody form) {
        // 用户登录
        LoginUser userInfo = sysLoginService.login(form.getUsername(), form.getPassword());
        // 获取登录token
        return R.ok(tokenService.createToken(userInfo));
    }
(2) sysLoginService(业务层。实现用户名和密码的相关验证,校验IP黑名单等功能)
   /**
     * 登录
     */
    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 (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;
    }
ServletUtils(客户端工具类-匹配IP黑名单。RequestContextHolder获取到的请求对象存储的IP和Redis中缓存的IP黑名单匹配)
    /**
     * 通过RequestContextHolder获取到的请求对象
     * @return ServletRequestAttributes
     */
    public static ServletRequestAttributes getRequestAttributes() {
        try {
            RequestAttributes attributes = RequestContextHolder.getRequestAttributes();
            return (ServletRequestAttributes) attributes;
        } catch (Exception e) {
            return null;
        }
    }
RemoteUserService(Feign远程调用-调用远程服务ruoyi-system中的接口根据用户名获取用户信息。使用@FeignClient指定value值即为调用的远程服务名称。加在方法上的注解@GetMapping的值"/user/info/{username}"则为请求的接口URL。@RequestHeader注解将请求头中SecurityConstants.FROM_SOURCE对应的静态常量值"from_source"与source变量绑定,即在请求头中"from_source"为键,source变量对应的值为键值,该键值会在后续内部服务调用验证处理时用到)

解释:Feign是一个基于接口注解的声明式Web服务客户端,它简化了使用HTTP请求远程服务的过程,使得我们可以通过定义接口的方式来调用远程服务。

@FeignClient(contextId = "remoteUserService", value = ServiceNameConstants.SYSTEM_SERVICE, fallbackFactory = RemoteUserFallbackFactory.class)
public interface RemoteUserService {
    /**
     * 通过用户名查询用户信息
     *
     * @param username 用户名
     * @param source   请求来源
     * @return 结果
     */
    @GetMapping("/user/info/{username}")
    public R<LoginUser> getUserInfo(@PathVariable("username") String username, @RequestHeader(SecurityConstants.FROM_SOURCE) String source);

    /**
SysUserController(ruoyi-system服务中的用户信息模块。接口上添加@InnerAuth自定义注解即意味着请求该接口会执行内部服务调用验证处理逻辑)
   /**
     * 获取当前用户信息
     */
    @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);
    }

InnerAuthAspect(自定义注解aop实现逻辑。接口上添加@InnerAuth自定义注解即意味着请求该接口会执行内部服务调用验证处理逻辑)
/**
 * 内部服务调用验证处理
 *实现了Ordered接口,重写getOrder方法用于定义切面的执行顺序
 * 
 */
@Aspect
@Component
public class InnerAuthAspect implements Ordered {
    @Around("@annotation(innerAuth)")
    public Object innerAround(ProceedingJoinPoint point, InnerAuth innerAuth) throws Throwable {
        String source = ServletUtils.getRequest().getHeader(SecurityConstants.FROM_SOURCE);
        // 内部请求验证
        if (!StringUtils.equals(SecurityConstants.INNER, source)) {
            throw new InnerAuthException("没有内部访问权限,不允许访问");
        }

        String userid = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USER_ID);
        String username = ServletUtils.getRequest().getHeader(SecurityConstants.DETAILS_USERNAME);
        // 用户信息验证
        if (innerAuth.isUser() && (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username))) {
            throw new InnerAuthException("没有设置用户信息,不允许访问 ");
        }
        //继续执行后续逻辑
        return point.proceed();
    }

    /**
     * 确保在权限认证aop执行前执行
     */
    @Override
    public int getOrder() {
        return Ordered.HIGHEST_PRECEDENCE + 1;
    }
}

SysUserMapper.xml、SysRoleMapper.xml、(持久层,从mysql数据库中获取用户登录信息,角色信息,权限信息等。)
获取用户登录信息
	<sql id="selectUserVo">
        select u.user_id, u.dept_id, u.user_name, u.nick_name, u.email, u.avatar, u.phonenumber, u.password, u.sex, u.status, u.del_flag, u.login_ip, u.login_date, u.create_by, u.create_time, u.remark, 
        d.dept_id, d.parent_id, d.ancestors, d.dept_name, d.order_num, d.leader, d.status as dept_status,
        r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.status as role_status
        from sys_user u
		    left join sys_dept d on u.dept_id = d.dept_id
		    left join sys_user_role ur on u.user_id = ur.user_id
		    left join sys_role r on r.role_id = ur.role_id
    </sql>
	<select id="selectUserByUserName" parameterType="String" resultMap="SysUserResult">
	    <include refid="selectUserVo"/>
		where u.user_name = #{userName} and u.del_flag = '0'
	</select>
获取用户角色信息
	<sql id="selectRoleVo">
	    select distinct r.role_id, r.role_name, r.role_key, r.role_sort, r.data_scope, r.menu_check_strictly, r.dept_check_strictly,
            r.status, r.del_flag, r.create_time, r.remark 
        from sys_role r
	        left join sys_user_role ur on ur.role_id = r.role_id
	        left join sys_user u on u.user_id = ur.user_id
	        left join sys_dept d on u.dept_id = d.dept_id
    </sql>
	<select id="selectRolePermissionByUserId" parameterType="Long" resultMap="SysRoleResult">
		<include refid="selectRoleVo"/>
		WHERE r.del_flag = '0' and ur.user_id = #{userId}
	</select>
获取用户菜单权限信息

单一角色获取权限信息

	<select id="selectMenuPermsByUserId" parameterType="Long" resultType="String">
		select distinct m.perms
		from sys_menu m
			 left join sys_role_menu rm on m.menu_id = rm.menu_id
			 left join sys_user_role ur on rm.role_id = ur.role_id
			 left join sys_role r on r.role_id = ur.role_id
		where m.status = '0' and r.status = '0' and ur.user_id = #{userId}
	</select>

多角色获取权限信息

	<select id="selectMenuPermsByRoleId" parameterType="Long" resultType="String">
		select distinct m.perms
		from sys_menu m
			 left join sys_role_menu rm on m.menu_id = rm.menu_id
		where m.status = '0' and rm.role_id = #{roleId}
	</select>
(3)TokenService(业务层。生成token令牌并存储在缓存redis中,同时返回给登录客户端)
   /**
     * 当用户登录时,通过传入一个LoginUser对象,该方法会创建一个令牌并返回包含令牌信息的Map。
     */
    public Map<String, Object> createToken(LoginUser loginUser)
    {
    	//使用IdUtils.fastUUID()方法生成一个唯一的令牌
        String token = IdUtils.fastUUID();
        //从与LoginUser关联的SysUser对象中获取用户ID
        Long userId = loginUser.getSysUser().getUserId();
        //从与LoginUser关联的SysUser对象中获取用户名
        String userName = loginUser.getSysUser().getUserName();
        //对象中设置生成的令牌
        loginUser.setToken(token);
        //在loginUser对象中设置用户ID
        loginUser.setUserid(userId);
        //在loginUser对象中设置用户名
        loginUser.setUsername(userName);
        //通过使用IpUtils.getIpAddr()方法获取用户的IP地址并设置
        loginUser.setIpaddr(IpUtils.getIpAddr());
        // 调用一个名为refreshToken的方法,传入loginUser对象以刷新一些与令牌相关的信息
        refreshToken(loginUser);

        // Jwt存储信息
        //创建一个新的HashMap来存储JWT令牌的声明
        Map<String, Object> claimsMap = new HashMap<String, Object>();
        //使用键USER_KEY将令牌添加到声明Map中
        claimsMap.put(SecurityConstants.USER_KEY, token);
        // 使用键DETAILS_USER_ID将用户ID添加到声明Map中
        claimsMap.put(SecurityConstants.DETAILS_USER_ID, userId);
        // 使用键DETAILS_USERNAME将用户名添加到声明Map中
        claimsMap.put(SecurityConstants.DETAILS_USERNAME, userName);

        // 接口返回信息
        // 使用键DETAILS_USERNAME将用户名添加到声明Map中
        Map<String, Object> rspMap = new HashMap<String, Object>();
        //通过使用声明Map创建JWT令牌,将访问令牌添加到响应Map中
        rspMap.put("access_token", JwtUtils.createToken(claimsMap));
        //将令牌的过期时间添加到响应Map中
        rspMap.put("expires_in", expireTime);
        //返回包含访问令牌和过期时间的响应Map
        return rspMap;
    }
(4)AuthFilter(全局过滤器。设定全局过滤器需要实现GlobalFilter, Ordered接口,该过滤器会判断请求url是否是url白名单,白名单则放行,否则获取请求头中的token,解析token后和redis中存储的token信息比较判断令牌是否有效)
/**
 * 网关鉴权
 *
 * @author ruoyi
 */
@Component
public class AuthFilter implements GlobalFilter, Ordered {
    private static final Logger log = LoggerFactory.getLogger(AuthFilter.class);

    // 排除过滤的 uri 地址,nacos自行添加
    @Autowired
    private IgnoreWhiteProperties ignoreWhite;

    @Autowired
    private RedisService redisService;


    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        ServerHttpRequest request = exchange.getRequest();
        ServerHttpRequest.Builder mutate = request.mutate();

        String url = request.getURI().getPath();
        // 跳过不需要验证的路径
        if (StringUtils.matches(url, ignoreWhite.getWhites())) {
            return chain.filter(exchange);
        }
        String token = getToken(request);
        if (StringUtils.isEmpty(token)) {
            return unauthorizedResponse(exchange, "令牌不能为空");
        }
        Claims claims = JwtUtils.parseToken(token);
        if (claims == null) {
            return unauthorizedResponse(exchange, "令牌已过期或验证不正确!");
        }
        String userkey = JwtUtils.getUserKey(claims);
        boolean islogin = redisService.hasKey(getTokenKey(userkey));
        if (!islogin) {
            return unauthorizedResponse(exchange, "登录状态已过期");
        }
        String userid = JwtUtils.getUserId(claims);
        String username = JwtUtils.getUserName(claims);
        if (StringUtils.isEmpty(userid) || StringUtils.isEmpty(username)) {
            return unauthorizedResponse(exchange, "令牌验证失败");
        }

        // 设置用户信息到请求
        addHeader(mutate, SecurityConstants.USER_KEY, userkey);
        addHeader(mutate, SecurityConstants.DETAILS_USER_ID, userid);
        addHeader(mutate, SecurityConstants.DETAILS_USERNAME, username);
        // 内部请求来源参数清除
        removeHeader(mutate, SecurityConstants.FROM_SOURCE);
        return chain.filter(exchange.mutate().request(mutate.build()).build());
    }

    private void addHeader(ServerHttpRequest.Builder mutate, String name, Object value) {
        if (value == null) {
            return;
        }
        String valueStr = value.toString();
        String valueEncode = ServletUtils.urlEncode(valueStr);
        mutate.header(name, valueEncode);
    }

    private void removeHeader(ServerHttpRequest.Builder mutate, String name) {
        mutate.headers(httpHeaders -> httpHeaders.remove(name)).build();
    }

    private Mono<Void> unauthorizedResponse(ServerWebExchange exchange, String msg) {
        log.error("[鉴权异常处理]请求路径:{}", exchange.getRequest().getPath());
        return ServletUtils.webFluxResponseWriter(exchange.getResponse(), msg, HttpStatus.UNAUTHORIZED);
    }

    /**
     * 获取缓存key
     */
    private String getTokenKey(String token) {
        return CacheConstants.LOGIN_TOKEN_KEY + token;
    }

    /**
     * 获取请求token
     */
    private String getToken(ServerHttpRequest request) {
        String token = request.getHeaders().getFirst(TokenConstants.AUTHENTICATION);
        // 如果前端设置了令牌前缀,则裁剪掉前缀
        if (StringUtils.isNotEmpty(token) && token.startsWith(TokenConstants.PREFIX)) {
            token = token.replaceFirst(TokenConstants.PREFIX, StringUtils.EMPTY);
        }
        return token;
    }

    @Override
    public int getOrder() {
        return -200;
    }
}
  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值