将 LoginUser 重构到 UserSessionService 模块汇总

This commit is contained in:
YunaiV
2021-01-29 22:47:03 +08:00
parent b557251b6f
commit ab94fe2d4b
21 changed files with 314 additions and 359 deletions

View File

@ -34,7 +34,4 @@ public class BaseDO implements Serializable {
@TableLogic
private Integer deleted;
// /** 备注 */ TODO 思考下,怎么解决
// private String remark;
}

View File

@ -28,6 +28,8 @@ import java.util.List;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
import static cn.iocoder.dashboard.framework.security.core.util.SecurityUtils.getLoginUserId;
import static cn.iocoder.dashboard.framework.security.core.util.SecurityUtils.getLoginUserRoleIds;
import static cn.iocoder.dashboard.util.servlet.ServletUtils.getClientIP;
import static cn.iocoder.dashboard.util.servlet.ServletUtils.getUserAgent;
@Api("认证 API")
@RestController
@ -47,7 +49,7 @@ public class SysAuthController {
@PostMapping("/login")
@OperateLog(enable = false) // 避免 Post 请求被记录操作日志
public CommonResult<SysAuthLoginRespVO> login(@RequestBody @Valid SysAuthLoginReqVO reqVO) {
String token = authService.login(reqVO.getUsername(), reqVO.getPassword(), reqVO.getUuid(), reqVO.getCode());
String token = authService.login(reqVO, getClientIP(), getUserAgent());
// 返回结果
return success(SysAuthLoginRespVO.builder().token(token).build());
}

View File

@ -0,0 +1,49 @@
package cn.iocoder.dashboard.modules.system.controller.auth;
import cn.iocoder.dashboard.common.pojo.CommonResult;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageItemRespVO;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageReqVO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth.SysUserSessionDO;
import cn.iocoder.dashboard.modules.system.service.auth.SysUserSessionService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiOperation;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import static cn.iocoder.dashboard.common.pojo.CommonResult.success;
@Api("用户 Session API")
@RestController
@RequestMapping("/user-session")
public class SysUserSessionController {
@Resource
private SysUserSessionService userSessionService;
@ApiOperation("获得 Session 分页列表")
@PreAuthorize("@ss.hasPermission('system:user-session:page')")
@GetMapping("/page")
public CommonResult<PageResult<SysUserSessionPageItemRespVO>> getUserSessionPage(@Validated SysUserSessionPageReqVO reqVO) {
// 获得 Session 分页
PageResult<SysUserSessionDO> sessionPage = userSessionService.getUserSessionPage(reqVO);
//
return null;
}
@ApiOperation("删除 Session")
@PreAuthorize("@ss.hasPermission('system:user-session:delete')")
@DeleteMapping("/delete")
@ApiImplicitParam(name = "id", value = "Session 编号", required = true, dataTypeClass = String.class,
example = "fe50b9f6-d177-44b1-8da9-72ea34f63db7")
public CommonResult<Boolean> delete(@RequestParam("id") String id) {
userSessionService.deleteUserSession(id);
return success(true);
}
}

View File

@ -0,0 +1,36 @@
package cn.iocoder.dashboard.modules.system.controller.auth.vo.session;
import cn.iocoder.dashboard.common.pojo.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
@ApiModel(value = "用户在线 Session Response VO", description = "相比用户基本信息来说,会多部门、用户账号等信息")
@Data
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = true)
public class SysUserSessionPageItemRespVO extends PageParam {
@ApiModelProperty(value = "Session 编号", required = true, example = "fe50b9f6-d177-44b1-8da9-72ea34f63db7")
private String id;
@ApiModelProperty(value = "用户 IP", required = true, example = "127.0.0.1")
private String userIp;
@ApiModelProperty(value = "浏览器 UserAgent", required = true, example = "Mozilla/5.0")
private String userAgent;
@ApiModelProperty(value = "登陆时间", required = true)
private String createTime;
@ApiModelProperty(value = "用户账号", required = true, example = "yudao")
private String username;
@ApiModelProperty(value = "部门名称", example = "研发部")
private String deptName;
}

View File

@ -2,11 +2,22 @@ package cn.iocoder.dashboard.modules.system.controller.auth.vo.session;
import cn.iocoder.dashboard.common.pojo.PageParam;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import lombok.EqualsAndHashCode;
import javax.validation.constraints.NotEmpty;
@ApiModel("在线用户 Session 分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
public class SysUserSessionPageReqVO extends PageParam {
@ApiModelProperty(value = "用户 IP", example = "127.0.0.1", notes = "模糊匹配")
@NotEmpty(message = "用户 IP 不能为空")
private String userIp;
@ApiModelProperty(value = "用户账号", example = "yudao", notes = "模糊匹配")
private String username;
}

View File

@ -2,6 +2,8 @@ package cn.iocoder.dashboard.modules.system.dal.mysql.dao.auth;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth.SysUserSessionDO;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
public interface SysUserOnlineMapper extends BaseMapper<SysUserSessionDO> {
@Mapper
public interface SysUserSessionMapper extends BaseMapper<SysUserSessionDO> {
}

View File

@ -1,17 +1,26 @@
package cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth;
import cn.iocoder.dashboard.framework.mybatis.core.dataobject.BaseDO;
import cn.iocoder.dashboard.framework.security.core.LoginUser;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Builder;
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* 在线用户表
*
* 我们已经将 {@link LoginUser} 缓存在 Redis 当中。
* 这里额外存储在线用户到 MySQL 中,目的是为了方便管理界面可以灵活查询。
* 同时,通过定时轮询 SysUserSessionDO 表,可以主动删除 Redis 的缓存,因为 Redis 的过期删除是延迟的。
*
* @author 芋道源码
*/
@TableName(value = "sys_user_session", autoResultMap = true)
@Data
@Builder
@EqualsAndHashCode(callSuper = true)
public class SysUserSessionDO extends BaseDO {

View File

@ -1,6 +1,7 @@
package cn.iocoder.dashboard.modules.system.service.auth;
import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFrameworkService;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.auth.SysAuthLoginReqVO;
/**
* 认证 Service 接口
@ -11,6 +12,14 @@ import cn.iocoder.dashboard.framework.security.core.service.SecurityAuthFramewor
*/
public interface SysAuthService extends SecurityAuthFrameworkService {
String login(String username, String password, String captchaUUID, String captchaCode);
/**
* 登陆用户
*
* @param reqVO 登陆信息
* @param userIp 用户 IP
* @param userAgent 用户 UA
* @return 身份令牌,使用 JWT 方式
*/
String login(SysAuthLoginReqVO reqVO, String userIp, String userAgent);
}

View File

@ -1,30 +0,0 @@
package cn.iocoder.dashboard.modules.system.service.auth;
import io.jsonwebtoken.Claims;
import java.util.Map;
/**
* Token Service 接口
*
* 提供访问 Token 令牌,目前基于 JWT 实现
*/
public interface SysTokenService {
/**
* 创建 Token
*
* @param subject 主体
* @return Token 字符串
*/
String createToken(String subject);
/**
* 解析 Token返回 claims 数据声明
*
* @param token Token
* @return claims
*/
Claims parseToken(String token);
}

View File

@ -1,40 +0,0 @@
package cn.iocoder.dashboard.modules.system.service.auth;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageReqVO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth.SysUserSessionDO;
import java.util.Date;
/**
* 在线用户 Session Service 接口
*/
public interface SysUserOnlineService {
/**
* 创建在线用户 Session
*
* @param sessionId Session 编号
* @param userId 用户编号
* @param userIp 用户 IP
* @param userAgent 用户 UA
*/
void createUserOnline(String sessionId, Long userId, String userIp, String userAgent);
/**
* 更新在线用户 Session 的更新时间
*
* @param sessionId Session 编号
* @param updateTime 更新时间
*/
void updateUserOnlineUpdateTime(String sessionId, Date updateTime);
/**
* 获得在线用户分页列表
*
* @param reqVO 分页条件
* @return 份额与列表
*/
PageResult<SysUserSessionDO> getUserSessionPage(SysUserSessionPageReqVO reqVO);
}

View File

@ -0,0 +1,63 @@
package cn.iocoder.dashboard.modules.system.service.auth;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.security.core.LoginUser;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageReqVO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth.SysUserSessionDO;
/**
* 在线用户 Session Service 接口
*
* @author 芋道源码
*/
public interface SysUserSessionService {
/**
* 创建在线用户 Session
*
* @param loginUser 登陆用户
* @param userIp 用户 IP
* @param userAgent 用户 UA
* @return Session 编号
*/
String createUserSession(LoginUser loginUser, String userIp, String userAgent);
/**
* 刷新在线用户 Session 的更新时间
*
* @param sessionId Session 编号
* @param loginUser 登陆用户
*/
void refreshUserSession(String sessionId, LoginUser loginUser);
/**
* 删除在线用户 Session
*
* @param sessionId Session 编号
*/
void deleteUserSession(String sessionId);
/**
* 获得 Session 编号对应的在线用户
*
* @param sessionId Session 编号
* @return 在线用户
*/
LoginUser getLoginUser(String sessionId);
/**
* 获得 Session 超时时间,单位:毫秒
*
* @return 超时时间
*/
Long getSessionTimeoutMillis();
/**
* 获得在线用户分页列表
*
* @param reqVO 分页条件
* @return 份额与列表
*/
PageResult<SysUserSessionDO> getUserSessionPage(SysUserSessionPageReqVO reqVO);
}

View File

@ -1,28 +1,22 @@
package cn.iocoder.dashboard.modules.system.service.auth.impl;
import cn.hutool.core.util.IdUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.dashboard.common.enums.CommonStatusEnum;
import cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil;
import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
import cn.iocoder.dashboard.framework.security.core.LoginUser;
import cn.iocoder.dashboard.framework.tracer.core.util.TracerUtils;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.auth.SysAuthLoginReqVO;
import cn.iocoder.dashboard.modules.system.controller.logger.vo.loginlog.SysLoginLogCreateReqVO;
import cn.iocoder.dashboard.modules.system.convert.auth.SysAuthConvert;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.user.SysUserDO;
import cn.iocoder.dashboard.modules.system.dal.redis.dao.auth.SysLoginUserRedisDAO;
import cn.iocoder.dashboard.modules.system.enums.logger.SysLoginLogTypeEnum;
import cn.iocoder.dashboard.modules.system.enums.logger.SysLoginResultEnum;
import cn.iocoder.dashboard.modules.system.service.auth.SysAuthService;
import cn.iocoder.dashboard.modules.system.service.auth.SysTokenService;
import cn.iocoder.dashboard.modules.system.service.auth.SysUserSessionService;
import cn.iocoder.dashboard.modules.system.service.common.SysCaptchaService;
import cn.iocoder.dashboard.modules.system.service.logger.SysLoginLogService;
import cn.iocoder.dashboard.modules.system.service.permission.SysPermissionService;
import cn.iocoder.dashboard.modules.system.service.user.SysUserService;
import cn.iocoder.dashboard.util.date.DateUtils;
import cn.iocoder.dashboard.util.servlet.ServletUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
@ -37,7 +31,6 @@ import org.springframework.util.Assert;
import javax.annotation.Resource;
import java.util.Collections;
import java.util.Date;
import java.util.Set;
import static cn.iocoder.dashboard.common.exception.util.ServiceExceptionUtil.exception;
@ -52,11 +45,6 @@ import static cn.iocoder.dashboard.modules.system.enums.SysErrorCodeConstants.*;
@Slf4j
public class SysAuthServiceImpl implements SysAuthService {
@Resource
private SecurityProperties securityProperties;
@Resource
private SysTokenService tokenService;
@Resource
private AuthenticationManager authenticationManager;
@Resource
@ -67,9 +55,8 @@ public class SysAuthServiceImpl implements SysAuthService {
private SysCaptchaService captchaService;
@Resource
private SysLoginLogService loginLogService;
@Resource
private SysLoginUserRedisDAO loginUserRedisDAO;
private SysUserSessionService userSessionService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
@ -91,27 +78,21 @@ public class SysAuthServiceImpl implements SysAuthService {
}
// 创建 LoginUser 对象
LoginUser loginUser = SysAuthConvert.INSTANCE.convert(user);
loginUser.setUpdateTime(new Date());
loginUser.setRoleIds(this.getUserRoleIds(loginUser.getId()));
loginUser.setRoleIds(this.getUserRoleIds(loginUser.getId())); // 获取用户角色列表
return loginUser;
}
@Override
public String login(String username, String password, String captchaUUID, String captchaCode) {
public String login(SysAuthLoginReqVO reqVO, String userIp, String userAgent) {
// 判断验证码是否正确
this.verifyCaptcha(username, captchaUUID, captchaCode);
this.verifyCaptcha(reqVO.getUsername(), reqVO.getUuid(), reqVO.getCode());
// 使用账号密码,进行登陆。
LoginUser loginUser = this.login0(username, password);
// 缓存登陆用户到 Redis 中
String sessionId = IdUtil.fastSimpleUUID();
loginUser.setUpdateTime(new Date());
loginUser.setRoleIds(this.getUserRoleIds(loginUser.getId()));
loginUserRedisDAO.set(sessionId, loginUser);
LoginUser loginUser = this.login0(reqVO.getUsername(), reqVO.getPassword());
loginUser.setRoleIds(this.getUserRoleIds(loginUser.getId())); // 获取用户角色列表
// 创建 Token
// 我们在返回给前端的 JWT 中,使用 sessionId 作为 subject 主体,标识当前 User 用户
return tokenService.createToken(sessionId);
// 缓存登陆用户到 Redis 中,返回 sessionId 编号
return userSessionService.createUserSession(loginUser, userIp, userAgent);
}
private void verifyCaptcha(String username, String captchaUUID, String captchaCode) {
@ -182,42 +163,20 @@ public class SysAuthServiceImpl implements SysAuthService {
@Override
public LoginUser verifyTokenAndRefresh(String token) {
// 验证 token 的有效性
String sessionId = this.verifyToken(token);
// 获得 LoginUser
LoginUser loginUser = loginUserRedisDAO.get(sessionId);
LoginUser loginUser = userSessionService.getLoginUser(token);
if (loginUser == null) {
return null;
}
// 刷新 LoginUser 缓存
this.refreshLoginUserCache(sessionId, loginUser);
this.refreshLoginUserCache(token, loginUser);
return loginUser;
}
private String verifyToken(String token) {
Claims claims;
try {
claims = tokenService.parseToken(token);
} catch (JwtException jwtException) {
log.warn("[verifyToken][token({}) 解析发生异常]", token);
return null;
}
// token 已经过期
if (DateUtils.isExpired(claims.getExpiration())) {
return null;
}
// 判断 sessionId 是否存在
String sessionId = claims.getSubject();
if (StrUtil.isBlank(sessionId)) {
return null;
}
return sessionId;
}
private void refreshLoginUserCache(String sessionId, LoginUser loginUser) {
private void refreshLoginUserCache(String token, LoginUser loginUser) {
// 每 1/3 的 Session 超时时间,刷新 LoginUser 缓存
if (System.currentTimeMillis() - loginUser.getUpdateTime().getTime() <
securityProperties.getSessionTimeout().toMillis() / 3) {
userSessionService.getSessionTimeoutMillis() / 3) {
return;
}
@ -229,9 +188,8 @@ public class SysAuthServiceImpl implements SysAuthService {
// 刷新 LoginUser 缓存
loginUser.setDeptId(user.getDeptId());
loginUser.setUpdateTime(new Date());
loginUser.setRoleIds(this.getUserRoleIds(loginUser.getId()));
loginUserRedisDAO.set(sessionId, loginUser);
userSessionService.refreshUserSession(token, loginUser);
}
}

View File

@ -1,58 +0,0 @@
package cn.iocoder.dashboard.modules.system.service.auth.impl;
import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
import cn.iocoder.dashboard.modules.system.service.auth.SysTokenService;
import cn.iocoder.dashboard.util.date.DateUtils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;
/**
* Token Service 实现类
*
* @author 芋道源码
*/
@Service
public class SysTokenServiceImpl implements SysTokenService {
@Resource
private SecurityProperties securityProperties;
@Override
public String createToken(String subject) {
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512, securityProperties.getTokenSecret())
.setExpiration(DateUtils.addTime(securityProperties.getTokenTimeout()))
.setSubject(subject)
.compact();
}
@Override
public Claims parseToken(String token) {
return Jwts.parser()
.setSigningKey(securityProperties.getTokenSecret())
.parseClaimsJws(token)
.getBody();
}
public static void main(String[] args) {
String secret = "abcdefghijklmnopqrstuvwxyz";
Map<String, Object> map = new HashMap<>();
map.put("key1", "value1");
System.out.println(Jwts.builder()
.signWith(SignatureAlgorithm.HS512, secret)
.setClaims(map)
.compact());
System.out.println(Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws("qyJhbGciOiJIUzUxMiJ9.eyJrZXkxIjoidmFsdWUxIn0.AHWncLRBlJkqrKaoWHZmMgbqYIT7rfLs8KCp9LuC0mdNfnx1xEMm1N9bgcD-0lc5sjySqsKiWzqJ3rpoyUSh0g")
.getBody());
}
}

View File

@ -0,0 +1,89 @@
package cn.iocoder.dashboard.modules.system.service.auth.impl;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.dashboard.common.pojo.PageResult;
import cn.iocoder.dashboard.framework.security.config.SecurityProperties;
import cn.iocoder.dashboard.framework.security.core.LoginUser;
import cn.iocoder.dashboard.modules.system.controller.auth.vo.session.SysUserSessionPageReqVO;
import cn.iocoder.dashboard.modules.system.dal.mysql.dao.auth.SysUserSessionMapper;
import cn.iocoder.dashboard.modules.system.dal.mysql.dataobject.auth.SysUserSessionDO;
import cn.iocoder.dashboard.modules.system.dal.redis.dao.auth.SysLoginUserRedisDAO;
import cn.iocoder.dashboard.modules.system.service.auth.SysUserSessionService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.Date;
/**
* 在线用户 Session Service 实现类
*
* @author 芋道源码
*/
@Service
public class SysUserSessionServiceImpl implements SysUserSessionService {
@Resource
private SecurityProperties securityProperties;
@Resource
private SysLoginUserRedisDAO loginUserRedisDAO;
@Resource
private SysUserSessionMapper userSessionMapper;
@Override
public String createUserSession(LoginUser loginUser, String userIp, String userAgent) {
// 生成 Session 编号
String sessionId = generateSessionId();
// 写入 Redis 缓存
loginUser.setUpdateTime(new Date());
loginUserRedisDAO.set(sessionId, loginUser);
// 写入 DB 中
SysUserSessionDO userSession = SysUserSessionDO.builder().userId(loginUser.getId())
.userIp(userIp).userAgent(userAgent).build();
userSessionMapper.insert(userSession);
// 返回 Session 编号
return sessionId;
}
@Override
public void refreshUserSession(String sessionId, LoginUser loginUser) {
// 写入 Redis 缓存
loginUser.setUpdateTime(new Date());
loginUserRedisDAO.set(sessionId, loginUser);
// 更新 DB 中
SysUserSessionDO updateObj = SysUserSessionDO.builder().id(sessionId).build();
updateObj.setUpdateTime(new Date());
userSessionMapper.updateById(updateObj);
}
@Override
public void deleteUserSession(String sessionId) {
}
@Override
public LoginUser getLoginUser(String sessionId) {
return loginUserRedisDAO.get(sessionId);
}
@Override
public Long getSessionTimeoutMillis() {
return securityProperties.getSessionTimeout().toMillis();
}
@Override
public PageResult<SysUserSessionDO> getUserSessionPage(SysUserSessionPageReqVO reqVO) {
return null;
}
/**
* 生成 Session 编号,目前采用 UUID 算法
*
* @return Session 编号
*/
private static String generateSessionId() {
return IdUtil.fastSimpleUUID();
}
}

View File

@ -41,6 +41,7 @@ spring:
# 芋道配置项,设置当前项目所有自定义的配置
yudao:
version: 1.0.0
web:
api-prefix: /api
controller-package: cn.iocoder.dashboard

View File

@ -0,0 +1,25 @@
芋道源码 http://www.iocoder.cn
Application Version: ${yudao.version}
Spring Boot Version: ${spring-boot.version}
////////////////////////////////////////////////////////////////////
// _ooOoo_ //
// o8888888o //
// 88" . "88 //
// (| ^_^ |) //
// O\ = /O //
// ____/`---'\____ //
// .' \\| |// `. //
// / \\||| : |||// \ //
// / _||||| -:- |||||- \ //
// | | \\\ - /// | | //
// | \_| ''\---/'' | | //
// \ .-\__ `-` ___/-. / //
// ___`. .' /--.--\ `. . ___ //
// ."" '< `.___\_<|>_/___.' >'"". //
// | | : `- \`.;`\ _ /`;.`/ - ` : | | //
// \ \ `-. \_ __\ /__ _/ .-` / / //
// ========`-.____`-.___\_____/___.-`____.-'======== //
// `=---=' //
// ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ //
// 佛祖保佑 永不宕机 永无BUG //
////////////////////////////////////////////////////////////////////