22 认证中心介绍
1 概述
- 用户身份认证的过程
- ruoyi-cloud认证中心的实现没有依赖任何插件,相对简单,一看就懂
- 从架构图的角度看认证中心:
- 登录请求,进到网关
- 网关直接调用认证中心。查看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,
}