一、登录
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;
}
}