1.用户相关
基本上都在下述类中实现:
/**
* 安全服务工具类
*/
public class SecurityFrameworkUtils {
/**
* HEADER 认证头 value 的前缀
*/
public static final String AUTHORIZATION_BEARER = "Bearer";
private SecurityFrameworkUtils() {}
/**
* 从请求中,获得认证 Token
*
* @param request 请求
* @param headerName 认证 Token 对应的 Header 名字
* @param parameterName 认证 Token 对应的 Parameter 名字
* @return 认证 Token
*/
public static String obtainAuthorization(HttpServletRequest request,
String headerName, String parameterName) {
// 1. 获得 Token。优先级:Header > Parameter
String token = request.getHeader(headerName);
if (StrUtil.isEmpty(token)) {
token = request.getParameter(parameterName);
}
if (!StringUtils.hasText(token)) {
return null;
}
// 2. 去除 Token 中带的 Bearer
int index = token.indexOf(AUTHORIZATION_BEARER + " ");
return index >= 0 ? token.substring(index + 7).trim() : token;
}
/**
* 获得当前认证信息
*
* @return 认证信息
*/
public static Authentication getAuthentication() {
SecurityContext context = SecurityContextHolder.getContext();
if (context == null) {
return null;
}
return context.getAuthentication();
}
/**
* 获取当前用户
*
* @return 当前用户
*/
@Nullable
public static LoginUser getLoginUser() {
Authentication authentication = getAuthentication();
if (authentication == null) {
return null;
}
return authentication.getPrincipal() instanceof LoginUser ? (LoginUser) authentication.getPrincipal() : null;
}
/**
* 获得当前用户的编号,从上下文中
*
* @return 用户编号
*/
@Nullable
public static Long getLoginUserId() {
LoginUser loginUser = getLoginUser();
return loginUser != null ? loginUser.getId() : null;
}
/**
* 获得当前用户的昵称,从上下文中
*
* @return 昵称
*/
@Nullable
public static String getLoginUserNickname() {
LoginUser loginUser = getLoginUser();
return loginUser != null ? MapUtil.getStr(loginUser.getInfo(), LoginUser.INFO_KEY_NICKNAME) : null;
}
/**
* 获得当前用户的部门编号,从上下文中
*
* @return 部门编号
*/
@Nullable
public static Long getLoginUserDeptId() {
LoginUser loginUser = getLoginUser();
return loginUser != null ? MapUtil.getLong(loginUser.getInfo(), LoginUser.INFO_KEY_DEPT_ID) : null;
}
/**
* 设置当前用户
*
* @param loginUser 登录用户
* @param request 请求
*/
public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
// 创建 Authentication,并设置到上下文
Authentication authentication = buildAuthentication(loginUser, request);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
// 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
}
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
// 创建 UsernamePasswordAuthenticationToken 对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
loginUser, null, Collections.emptyList());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
return authenticationToken;
}
}
1.1 获取当前用户信息
public static LoginUser getLoginUser()
1.2 获取当前用户编号(最常用)
public static Long getLoginUserId()
1.3 获取当前用户昵称
public static LoginUser getLoginUserNickname()
1.4 获取当前用户部门
public static Long getLoginUserDeptId()
1.5 设置当前用户
public static void setLoginUser(LoginUser loginUser, HttpServletRequest request)
/**
* 设置当前用户
*
* @param loginUser 登录用户
* @param request 请求
*/
public static void setLoginUser(LoginUser loginUser, HttpServletRequest request) {
// 创建 Authentication,并设置到上下文
Authentication authentication = buildAuthentication(loginUser, request);
SecurityContextHolder.getContext().setAuthentication(authentication);
// 额外设置到 request 中,用于 ApiAccessLogFilter 可以获取到用户编号;
// 原因是,Spring Security 的 Filter 在 ApiAccessLogFilter 后面,在它记录访问日志时,线上上下文已经没有用户编号等信息
WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
}
private static Authentication buildAuthentication(LoginUser loginUser, HttpServletRequest request) {
// 创建 UsernamePasswordAuthenticationToken 对象
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
loginUser, null, Collections.emptyList());
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
return authenticationToken;
}
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, Collections.emptyList());
(1) 创建 UsernamePasswordAuthenticationToken
-
参数:
-
principal(loginUser):认证主体,这里放入自定义用户对象 -
credentials(null):凭证(密码),这里为 null 表示不验证密码 -
authorities(Collections.emptyList()):权限列表,初始为空
-
-
UsernamePasswordAuthenticationToken是 Spring Security 的核心类,继承自AbstractAuthenticationToken,实现了Authentication接口。它的设计目的是封装用户名/密码形式的认证信息。
(2) 设置请求详情
authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
-
作用:将 HTTP 请求的详细信息(如 IP、SessionID 等)存入认证对象
-
组件:
-
WebAuthenticationDetailsSource:Spring Security 提供的工具类 -
buildDetails():提取请求中的详细信息(如 remoteAddress、sessionId 等)
-
SecurityContextHolder.getContext().setAuthentication(authentication);
-
SecurityContextHolder:Spring Security 的核心存储容器-
默认使用
ThreadLocal存储(与当前线程绑定) -
后续通过
SecurityContextHolder.getContext()可全局获取
-
-
SecurityContext:包含当前线程的完整安全信息
WebFrameworkUtils.setLoginUserId(request, loginUser.getId());
WebFrameworkUtils.setLoginUserType(request, loginUser.getUserType());
HTTP Request
→ TokenAuthenticationFilter(自定义)
→ ApiAccessLogFilter(日志记录)
→ Spring Security 过滤器链(认证/授权)
→ Controller
因为我们的 setLoginUser在TokenAuthenticationFilter之中引用,即可保证在日志记录前可以拿到用户信息;
-
HttpServletRequest的特性:-
同一个请求的整个生命周期中,
request对象是同一个 -
通过
setAttribute()设置的值可在后续所有过滤器和Controller中获取
-
2.账号密码登录
@PostMapping("/login")
@PermitAll
@Operation(summary = "使用账号密码登录")
public CommonResult<AuthLoginRespVO> login(@RequestBody @Valid AuthLoginReqVO reqVO) {
return success(authService.login(reqVO));
}
@PostMapping("/logout")
@PermitAll
@Operation(summary = "登出系统")
public CommonResult<Boolean> logout(HttpServletRequest request) {
String token = SecurityFrameworkUtils.obtainAuthorization(request,
securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
if (StrUtil.isNotBlank(token)) {
authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType());
}
return success(true);
}
@Override
public AuthLoginRespVO login(AuthLoginReqVO reqVO) {
// 校验验证码
validateCaptcha(reqVO);
// 使用账号密码,进行登录
AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword());
// 如果 socialType 非空,说明需要绑定社交用户(什么方法登录的时候传socialtype)
if (reqVO.getSocialType() != null) {
socialUserService.bindSocialUser(new SocialUserBindReqDTO(user.getId(), getUserType().getValue(),
reqVO.getSocialType(), reqVO.getSocialCode(), reqVO.getSocialState()));
}
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME);
}
基本上就是校验验证码、账号是否存在、账号密码是否匹配、是否需要绑定社交用户、插入登录日志、构建令牌(创建刷新令牌、获取客户端信息、校验客户端信息(密钥,授权,回调地址))、构建刷新令牌、根据刷新令牌构建访问令牌;
3. 三方登录
三方登录完成时,系统会将三方用户存储到 system_social_user表中,通过 type 标记对应的第三方平台。
【未】关联本系统 User 的三方用户,需要在三方登录完成后,使用账号密码进行「绑定登录」,成功后记录到 system_social_user_bind 表中。
【已】关联本系统 User 的三方用户,在三方登录完成后,直接进入系统,即「快捷登录」。
//三方登录接口,接收前端请求,生成第三方社交平台的授权跳转 URL,并返回给前端
@GetMapping("/social-auth-redirect")
@PermitAll
@Operation(summary = "社交授权的跳转")
@Parameters({
@Parameter(name = "type", description = "社交类型", required = true),
@Parameter(name = "redirectUri", description = "回调路径")
})
public CommonResult<String> socialLogin(@RequestParam("type") Integer type,
@RequestParam("redirectUri") String redirectUri) {
return success(socialClientService.getAuthorizeUrl(
type, UserTypeEnum.ADMIN.getValue(), redirectUri));
}
@Override
public String getAuthorizeUrl(Integer socialType, Integer userType, String redirectUri) {
// 获得对应的 AuthRequest 实现
AuthRequest authRequest = buildAuthRequest(socialType, userType);
// 生成跳转地址
String authorizeUri = authRequest.authorize(AuthStateUtils.createState());
return HttpUtils.replaceUrlQuery(authorizeUri, "redirect_uri", redirectUri);
}
-
构建 AuthRequest 对象
-
调用
buildAuthRequest(socialType, userType),根据社交类型和用户类型获取对应的AuthRequest(由JustAuth框架提供)。
-
-
生成授权 URL
-
调用
authRequest.authorize(),生成带随机state参数的授权 URL(防止 CSRF 攻击)。
-
-
替换回调地址
-
使用
HttpUtils.replaceUrlQuery()将授权 URL 中的redirect_uri参数替换为前端传入的redirectUri。
-
AuthRequest:这是第三方登录 SDK(如JustAuth,一个流行的 Java 第三方登录工具库)中的核心接口,封装了不同社交平台的授权逻辑(如微信、GitHub 的授权流程)。每个社交平台有对应的实现类(如WechatAuthRequest、GithubAuthRequest)。authRequest.authorize(String state):AuthRequest接口的原生方法,用于生成第三方平台的授权 URL。参数state是一个随机字符串,用于防止 CSRF 攻击(第三方平台授权成功后会原样返回该值,后端可验证一致性)。AuthStateUtils.createState():SDK 提供的工具类方法,用于生成随机的state值(通常是 UUID 或随机字符串)。HttpUtils.replaceUrlQuery(String url, String key, String value):自定义工具类方法,用于替换 URL 中的查询参数(这里替换redirect_uri,确保回调路径正确)。
构建授权请求对象:buildAuthRequest 方法
- 构建并返回对应社交平台的
AuthRequest对象,核心是处理配置优先级:数据库配置覆盖默认配置(默认配置通常在application.yaml中)。 - 场景:同一社交平台(如微信)可能对不同用户类型(管理员 / 普通用户)有不同的
clientId和clientSecret(第三方平台的开发者凭证),因此需要从数据库读取对应配置并覆盖默认值。
-
获取默认配置的 AuthRequest:
SocialTypeEnum.valueOfType(socialType).getSource():将前端传入的type(Integer)转换为社交平台的标识(如 “wechat”、“github”)。authRequestFactory.get(String source):从工厂类中获取该平台默认配置的AuthRequest对象(默认配置来自application.yaml,如justauth.configs.wechat.client-id等)。Assert.notNull:校验平台是否存在,不存在则抛出异常。
-
查询数据库配置并覆盖:
socialClientMapper.selectBySocialTypeAndUserType(...):从数据库查询该社交平台 + 用户类型的配置(SocialClientDO包含clientId、clientSecret等开发者凭证)。- 若数据库配置存在且启用(
status=ENABLE),则用数据库配置覆盖默认配置:ReflectUtil.getFieldValue(request, "config"):通过反射获取AuthRequest对象中的config字段(AuthConfig类型,封装了clientId、clientSecret等配置)。ReflectUtil.newInstance(authConfig.getClass()):通过反射创建AuthConfig的新实例(避免修改原对象影响默认配置)。BeanUtil.copyProperties:复制默认配置到新对象(保留默认配置中的非关键字段)。- 修改新对象的
clientId、clientSecret(从数据库读取),并通过反射设置回AuthRequest对象中。
关键原生方法 / 工具类解析:
AuthConfig:第三方 SDK 中的配置类,封装了第三方平台的开发者凭证(clientId、clientSecret)、授权范围、回调地址等信息。ReflectUtil:可能是 Hutool 等工具库中的反射工具类,提供getFieldValue(获取对象字段值)、newInstance(通过反射创建对象)、setFieldValue(设置对象字段值)等方法。这里使用反射是因为AuthRequest的config字段是私有字段,且没有提供 setter 方法,只能通过反射修改。BeanUtil.copyProperties:工具类方法(如 Hutool 的 BeanUtil),用于复制两个对象的属性(这里将默认配置的属性复制到新对象,只修改需要覆盖的字段)。SocialClientMapper.selectBySocialTypeAndUserType:MyBatis 的 Mapper 方法,用于从数据库查询社交平台配置(ORM 操作)。
-
前端拿到授权链接后,会跳转到第三方平台的登录授权页面(如微信扫码页)。
-
用户确认授权后,第三方平台会将用户重定向到步骤 1 中传入的
redirectUri,并在 URL 中携带两个关键参数:code:授权码(第三方平台临时发放的凭证,用于换取令牌)。state:步骤 1 中AuthStateUtils.createState()生成的随机串(用于验证请求合法性,防止 CSRF 攻击)。
例如,重定向后的 URL 可能是:
https://xxx.com/callback?code=ABC123&state=9b2ffbc1-7425-4155-9894-9d5c08541d62
//用授权结果完成实际登录
@PostMapping("/social-login")
@PermitAll
@Operation(summary = "社交快捷登录,使用 code 授权码", description = "适合未登录的用户,但是社交账号已绑定用户")
public CommonResult<AuthLoginRespVO> socialQuickLogin(@RequestBody @Valid AuthSocialLoginReqVO reqVO) {
return success(authService.socialLogin(reqVO));
}
@Override
public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) {
// 使用 code 授权码,进行登录。然后,获得到绑定的用户编号
SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(UserTypeEnum.ADMIN.getValue(), reqVO.getType(),
reqVO.getCode(), reqVO.getState());
if (socialUser == null || socialUser.getUserId() == null) {
throw exception(AUTH_THIRD_LOGIN_NOT_BIND);
}
// 获得用户
AdminUserDO user = userService.getUser(socialUser.getUserId());
if (user == null) {
throw exception(USER_NOT_EXISTS);
}
// 创建 Token 令牌,记录登录日志
return createTokenAfterLoginSuccess(user.getId(), user.getUsername(), LoginLogTypeEnum.LOGIN_SOCIAL);
}
@Override
public SocialUserRespDTO getSocialUserByCode(Integer userType, Integer socialType, String code, String state) {
// 获得社交用户
SocialUserDO socialUser = authSocialUser(socialType, userType, code, state);
Assert.notNull(socialUser, "社交用户不能为空");
// 获得绑定用户
SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserTypeAndSocialUserId(userType,
socialUser.getId());
return new SocialUserRespDTO(socialUser.getOpenid(), socialUser.getNickname(), socialUser.getAvatar(),
socialUserBind != null ? socialUserBind.getUserId() : null);
}
/**
* 授权获得对应的社交用户
* 如果授权失败,则会抛出 {@link ServiceException} 异常
*
* @param socialType 社交平台的类型 {@link SocialTypeEnum}
* @param userType 用户类型
* @param code 授权码
* @param state state
* @return 授权用户
*/
@NotNull
public SocialUserDO authSocialUser(Integer socialType, Integer userType, String code, String state) {
// 优先从 DB 中获取,因为 code 有且可以使用一次。
// 在社交登录时,当未绑定 User 时,需要绑定登录,此时需要 code 使用两次
SocialUserDO socialUser = socialUserMapper.selectByTypeAndCodeAnState(socialType, code, state);
if (socialUser != null) {
return socialUser;
}
// 请求获取
AuthUser authUser = socialClientService.getAuthUser(socialType, userType, code, state);
Assert.notNull(authUser, "三方用户不能为空");
// 保存到 DB 中
socialUser = socialUserMapper.selectByTypeAndOpenid(socialType, authUser.getUuid());
if (socialUser == null) {
socialUser = new SocialUserDO();
}
socialUser.setType(socialType).setCode(code).setState(state) // 需要保存 code + state 字段,保证后续可查询
.setOpenid(authUser.getUuid()).setToken(authUser.getToken().getAccessToken()).setRawTokenInfo((toJsonString(authUser.getToken())))
.setNickname(authUser.getNickname()).setAvatar(authUser.getAvatar()).setRawUserInfo(toJsonString(authUser.getRawUserInfo()));
if (socialUser.getId() == null) {
socialUserMapper.insert(socialUser);
} else {
socialUserMapper.updateById(socialUser);
}
return socialUser;
}
前端调用 social-login 接口,用 code 完成登录
- 前端从 URL 中解析出
code和state,再携带type(社交平台类型),调用后端social-login接口。 - 后端通过
socialLogin方法完成登录:- 调用
socialUserService.getSocialUserByCode(...):用code向第三方平台换取access_token,再用令牌获取用户的第三方账号信息(如微信的 openid、昵称等)。 - 检查该第三方账号是否已绑定本地系统用户(
socialUser.getUserId()是否存在):- 若已绑定:获取本地用户信息(
AdminUserDO),生成登录令牌(Token),返回登录结果。 - 若未绑定:抛出「未绑定账号」异常(
AUTH_THIRD_LOGIN_NOT_BIND),通常需要引导用户进行账号绑定。
- 若已绑定:获取本地用户信息(
- 调用
3. 关键参数的作用(为何需要 code 和 state)
-
code:授权码是第三方平台给前端的「临时凭证」,前端必须将其传给后端,由后端用code向第三方平台换取access_token(令牌)。- 为什么不直接在前端用
code换access_token?
因为access_token是访问用户信息的核心凭证,必须在后端处理(避免前端泄露令牌导致安全问题)。
- 为什么不直接在前端用
-
state:步骤 1 中生成的随机串,步骤 3 中会验证其有效性(通常后端会暂存state,并在social-login时校验)。- 作用:防止 CSRF 攻击(攻击者无法伪造
state,确保授权请求是用户主动发起的)。
- 作用:防止 CSRF 攻击(攻击者无法伪造
ocialLogin() → getSocialUserByCode() → authSocialUser()
最终目的是:通过code和state获取第三方用户信息,验证其与本地用户的绑定关系,完成登录
socialLogin():登录入口,验证绑定关系并生成令牌
@Override
public AuthLoginRespVO socialLogin(AuthSocialLoginReqVO reqVO) {
// 1. 用 code 和 state 获取社交用户信息及绑定关系
SocialUserRespDTO socialUser = socialUserService.getSocialUserByCode(
UserTypeEnum.ADMIN.getValue(), // 用户类型(管理员)
reqVO.getType(), // 社交平台类型(如微信=1)
reqVO.getCode(), // 授权码 code
reqVO.getState() // 防CSRF的 state
);
// 2. 校验:社交用户是否已绑定本地用户
if (socialUser == null || socialUser.getUserId() == null) {
throw exception(AUTH_THIRD_LOGIN_NOT_BIND); // 未绑定则抛出异常
}
// 3. 获取本地用户信息
AdminUserDO user = userService.getUser(socialUser.getUserId());
if (user == null) {
throw exception(USER_NOT_EXISTS); // 本地用户不存在则抛出异常
}
// 4. 生成登录令牌,记录登录日志
return createTokenAfterLoginSuccess(
user.getId(),
user.getUsername(),
LoginLogTypeEnum.LOGIN_SOCIAL // 登录类型:社交登录
);
}
- 调用
getSocialUserByCode获取第三方用户信息及绑定的本地用户 ID。 - 校验绑定关系(必须已绑定本地用户才能登录)。
- 生成登录令牌(Token),完成登录流程。
getSocialUserByCode():关联社交用户与本地用户
@Override
public SocialUserRespDTO getSocialUserByCode(
Integer userType, Integer socialType, String code, String state) {
// 1. 获取第三方用户的原始信息(从第三方平台或本地数据库)
SocialUserDO socialUser = authSocialUser(socialType, userType, code, state);
Assert.notNull(socialUser, "社交用户不能为空"); // 校验:必须获取到社交用户
// 2. 查询该社交用户是否已绑定本地用户
SocialUserBindDO socialUserBind = socialUserBindMapper.selectByUserTypeAndSocialUserId(
userType,
socialUser.getId() // 社交用户在本地数据库的ID
);
// 3. 封装结果:社交用户信息 + 绑定的本地用户ID(可能为null)
return new SocialUserRespDTO(
socialUser.getOpenid(), // 第三方平台的唯一标识(如微信openid)
socialUser.getNickname(), // 第三方用户昵称
socialUser.getAvatar(), // 第三方用户头像
// 若已绑定,返回本地用户ID;否则为null
socialUserBind != null ? socialUserBind.getUserId() : null
);
}
- 调用
authSocialUser获取第三方用户在本地数据库的记录(SocialUserDO)。 - 通过
socialUserBindMapper查询该社交用户是否绑定了本地用户(SocialUserBindDO是关联表,记录社交用户ID与本地用户ID的映射)。 - 返回封装了社交用户信息和绑定状态的
SocialUserRespDTO
authSocialUser():获取并持久化第三方用户信息
@NotNull
public SocialUserDO authSocialUser(
Integer socialType, Integer userType, String code, String state) {
// 步骤1:先查本地数据库,是否已有该 code + state 对应的社交用户
// 原因:code 理论上只能用一次,但未绑定时可能需要重复使用(如用户授权后未立即绑定)
SocialUserDO socialUser = socialUserMapper.selectByTypeAndCodeAnState(
socialType, code, state);
if (socialUser != null) {
return socialUser; // 本地已有记录,直接返回
}
// 步骤2:本地无记录,调用第三方平台接口,用 code 和 state 获取用户信息
AuthUser authUser = socialClientService.getAuthUser(
socialType, userType, code, state);
Assert.notNull(authUser, "三方用户不能为空"); // 校验:必须获取到第三方用户信息
// 步骤3:查询本地是否已有该第三方用户(通过 openid 唯一标识)
// 第三方用户的唯一标识:authUser.getUuid()(如微信的openid,GitHub的id)
socialUser = socialUserMapper.selectByTypeAndOpenid(socialType, authUser.getUuid());
// 步骤4:新建或更新本地社交用户记录
if (socialUser == null) {
socialUser = new SocialUserDO(); // 本地无记录,新建对象
}
// 填充/更新社交用户信息
socialUser.setType(socialType) // 社交平台类型(如微信)
.setCode(code) // 保存授权码(用于重复使用场景)
.setState(state) // 保存state(用于验证)
.setOpenid(authUser.getUuid()) // 第三方平台唯一标识(如openid)
.setToken(authUser.getToken().getAccessToken()) // 第三方平台的访问令牌
.setRawTokenInfo(toJsonString(authUser.getToken())) // 令牌原始信息(JSON)
.setNickname(authUser.getNickname()) // 昵称
.setAvatar(authUser.getAvatar()) // 头像
.setRawUserInfo(toJsonString(authUser.getRawUserInfo())); // 原始用户信息(JSON)
// 步骤5:保存到数据库(新增或更新)
if (socialUser.getId() == null) {
socialUserMapper.insert(socialUser); // 新增
} else {
socialUserMapper.updateById(socialUser); // 更新
}
return socialUser;
}
- 优先从本地数据库获取社交用户(避免重复调用第三方接口,处理
code可能重复使用的场景)。 - 若本地无记录,调用
socialClientService.getAuthUser(未展示代码)向第三方平台发起请求:用code换取access_token,再用令牌获取用户信息(封装为AuthUser对象,包含第三方用户的唯一标识、昵称、头像等)。 - 将第三方用户信息持久化到本地数据库(
SocialUserDO表),方便后续查询和绑定操作
总结
- 用前端传递的
code和state从第三方平台获取用户信息。 - 将第三方用户信息保存到本地数据库,形成
SocialUserDO记录。 - 检查该第三方用户是否已绑定本地用户(通过
SocialUserBindDO)。 - 若已绑定,生成本地系统的登录令牌,完成登录。
4. 用户登出
@PostMapping("/logout")
@PermitAll
@Operation(summary = "登出系统")
public CommonResult<Boolean> logout(HttpServletRequest request) {
String token = SecurityFrameworkUtils.obtainAuthorization(request,
securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
if (StrUtil.isNotBlank(token)) {
authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType());
}
return success(true);
}
/**
* 从请求中,获得认证 Token
*
* @param request 请求
* @param headerName 认证 Token 对应的 Header 名字
* @param parameterName 认证 Token 对应的 Parameter 名字
* @return 认证 Token
*/
public static String obtainAuthorization(HttpServletRequest request,
String headerName, String parameterName) {
// 1. 获得 Token。优先级:Header > Parameter
String token = request.getHeader(headerName);
if (StrUtil.isEmpty(token)) {
token = request.getParameter(parameterName);
}
if (!StringUtils.hasText(token)) {
return null;
}
// 2. 去除 Token 中带的 Bearer
int index = token.indexOf(AUTHORIZATION_BEARER + " ");
return index >= 0 ? token.substring(index + 7).trim() : token;
}
@ConfigurationProperties(prefix = "moyun.security")
@Validated
@Data
public class SecurityProperties {
/**
* HTTP 请求时,访问令牌的请求 Header
*/
@NotEmpty(message = "Token Header 不能为空")
private String tokenHeader = "Authorization";
/**
* HTTP 请求时,访问令牌的请求参数
*
* 初始目的:解决 WebSocket 无法通过 header 传参,只能通过 token 参数拼接
*/
@NotEmpty(message = "Token Parameter 不能为空")
private String tokenParameter = "token";
/**
* mock 模式的开关
*/
@NotNull(message = "mock 模式的开关不能为空")
private Boolean mockEnable = false;
/**
* mock 模式的密钥
* 一定要配置密钥,保证安全性
*/
@NotEmpty(message = "mock 模式的密钥不能为空") // 这里设置了一个默认值,因为实际上只有 mockEnable 为 true 时才需要配置。
private String mockSecret = "test";
/**
* 免登录的 URL 列表
*/
private List<String> permitAllUrls = Collections.emptyList();
/**
* PasswordEncoder 加密复杂度,越高开销越大
*/
private Integer passwordEncoderLength = 4;
}
@Override
public void logout(String token, Integer logType) {
// 删除访问令牌
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token);
if (accessTokenDO == null) {
return;
}
// 删除成功,则记录登出日志
createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType);
}
private void createLogoutLog(Long userId, Integer userType, Integer logType) {
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
reqDTO.setLogType(logType);
reqDTO.setTraceId(TracerUtils.getTraceId());
reqDTO.setUserId(userId);
reqDTO.setUserType(userType);
if (ObjectUtil.equal(getUserType().getValue(), userType)) {
reqDTO.setUsername(getUsername(userId));
} else {
reqDTO.setUsername(memberService.getMemberUserMobile(userId));
}
reqDTO.setUserAgent(ServletUtils.getUserAgent());
reqDTO.setUserIp(ServletUtils.getClientIP());
reqDTO.setResult(LoginResultEnum.SUCCESS.getResult());
loginLogService.createLoginLog(reqDTO);
}
一、控制器接口:logout 方法
@PostMapping("/logout")
@PermitAll
@Operation(summary = "登出系统")
public CommonResult<Boolean> logout(HttpServletRequest request) {
// 1. 从请求中获取 Token
String token = SecurityFrameworkUtils.obtainAuthorization(request,
securityProperties.getTokenHeader(), securityProperties.getTokenParameter());
// 2. 若 Token 存在,则执行登出逻辑
if (StrUtil.isNotBlank(token)) {
authService.logout(token, LoginLogTypeEnum.LOGOUT_SELF.getType());
}
// 3. 返回登出成功
return success(true);
}
作用:
- 这是处理登出请求的入口接口,支持所有用户访问(
@PermitAll,即使未登录也能调用)。 - 核心流程:从请求中提取令牌 → 调用服务层执行登出 → 返回成功结果。
二、令牌提取工具:obtainAuthorization 方法
public static String obtainAuthorization(HttpServletRequest request,
String headerName, String parameterName) {
// 1. 优先从 Header 中获取 Token(主流方式)
String token = request.getHeader(headerName);
// 2. 若 Header 中没有,则从请求参数中获取(适配特殊场景,如 WebSocket)
if (StrUtil.isEmpty(token)) {
token = request.getParameter(parameterName);
}
// 3. 若 Token 为空,直接返回 null
if (!StringUtils.hasText(token)) {
return null;
}
// 4. 去除 Token 中的 "Bearer " 前缀(如果有)
// 注:OAuth2 规范中,Token 通常以 "Bearer <token>" 形式放在 Header 中
int index = token.indexOf(AUTHORIZATION_BEARER + " ");
return index >= 0 ? token.substring(index + 7).trim() : token;
}
作用:
- 从请求中提取有效的令牌字符串,处理两种传递方式和格式兼容。
- 关键逻辑:
- 优先级:
Header > 请求参数(Header 是标准方式,参数用于特殊场景如 WebSocket)。 - 格式处理:自动去除
Bearer前缀(例如将Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...转换为纯令牌)。
- 优先级:
三、安全配置属性:SecurityProperties 类
@ConfigurationProperties(prefix = "moyun.security") // 从配置文件读取前缀为 moyun.security 的配置
@Data
public class SecurityProperties {
// Token 在 Header 中的键名(默认 "Authorization")
private String tokenHeader = "Authorization";
// Token 在请求参数中的键名(默认 "token",用于 WebSocket 等场景)
private String tokenParameter = "token";
// 其他配置...(mock 模式、免登录 URL 等)
}
作用:
- 封装系统的安全配置,支持通过配置文件(如
application.yaml)自定义 Token 的传递方式。 - 例如,若配置
moyun.security.token-header: X-Token,则 Token 会从 Header 的X-Token字段提取。
四、服务层登出逻辑:logout 方法
@Override
public void logout(String token, Integer logType) {
// 1. 删除令牌(使 Token 失效)
OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.removeAccessToken(token);
if (accessTokenDO == null) { // 令牌不存在或已失效,直接返回
return;
}
// 2. 记录登出日志
createLogoutLog(accessTokenDO.getUserId(), accessTokenDO.getUserType(), logType);
}
作用:
- 核心登出逻辑:使令牌失效 + 记录日志。
- 关键步骤:
oauth2TokenService.removeAccessToken(token):删除存储的令牌(通常存在 Redis 或数据库中),确保该 Token 无法再用于访问系统。createLogoutLog(...):生成登出日志,记录谁在何时何地登出。
五、登出日志记录:createLogoutLog 方法
private void createLogoutLog(Long userId, Integer userType, Integer logType) {
LoginLogCreateReqDTO reqDTO = new LoginLogCreateReqDTO();
reqDTO.setLogType(logType); // 日志类型:主动登出(LOGOUT_SELF)
reqDTO.setTraceId(TracerUtils.getTraceId()); // 分布式追踪 ID,用于链路分析
reqDTO.setUserId(userId); // 登出用户的 ID
reqDTO.setUserType(userType); // 用户类型(如管理员、普通用户)
// 设置用户名(根据用户类型从不同表查询)
if (ObjectUtil.equal(getUserType().getValue(), userType)) {
reqDTO.setUsername(getUsername(userId)); // 管理员用户
} else {
reqDTO.setUsername(memberService.getMemberUserMobile(userId)); // 普通用户(用手机号作为用户名)
}
// 记录客户端信息
reqDTO.setUserAgent(ServletUtils.getUserAgent()); // 浏览器/设备信息
reqDTO.setUserIp(ServletUtils.getClientIP()); // 客户端 IP 地址
reqDTO.setResult(LoginResultEnum.SUCCESS.getResult()); // 登出结果:成功
// 保存日志
loginLogService.createLoginLog(reqDTO);
}
作用:
- 生成详细的登出日志,用于系统审计和问题排查。
- 日志包含的关键信息:用户 ID、用户类型、登出时间、客户端 IP、设备信息、登出结果等。
整体流程总结
- 前端发起登出请求:调用
/logout接口,通过 Header 或参数传递当前登录的 Token。 - 后端提取 Token:
obtainAuthorization方法从请求中解析出有效 Token。 - 令牌失效处理:
oauth2TokenService.removeAccessToken删除存储的 Token,确保其无法再使用。 - 记录登出日志:
createLogoutLog生成日志,记录登出详情。 - 返回成功响应:告知前端登出完成。
9424

被折叠的 条评论
为什么被折叠?



