实现管理后台登录时,使用 OAuth2 的 access token

pull/2/head
YunaiV 2022-05-08 23:52:31 +08:00
parent ebee4ddb7c
commit 4f52d1367b
30 changed files with 626 additions and 357 deletions

View File

@ -44,18 +44,11 @@
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<!-- TODO 芋艿: --> <!-- 业务组件 -->
<dependency> <dependency>
<groupId>org.activiti</groupId> <groupId>cn.iocoder.boot</groupId>
<artifactId>activiti-engine</artifactId> <artifactId>yudao-module-system-api</artifactId> <!-- 需要使用它,进行 Token 的校验 -->
<version>7.1.0.M6</version> <version>${revision}</version>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
<optional>true</optional>
</dependency> </dependency>
</dependencies> </dependencies>

View File

@ -6,6 +6,7 @@ import cn.iocoder.yudao.framework.security.core.filter.TokenAuthenticationFilter
import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl; import cn.iocoder.yudao.framework.security.core.handler.AccessDeniedHandlerImpl;
import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl; import cn.iocoder.yudao.framework.security.core.handler.AuthenticationEntryPointImpl;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import cn.iocoder.yudao.module.system.api.auth.OAuth2TokenApi;
import org.springframework.beans.factory.config.MethodInvokingFactoryBean; import org.springframework.beans.factory.config.MethodInvokingFactoryBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@ -72,8 +73,9 @@ public class YudaoSecurityAutoConfiguration {
* Token Bean * Token Bean
*/ */
@Bean @Bean
public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler) { public TokenAuthenticationFilter authenticationTokenFilter(GlobalExceptionHandler globalExceptionHandler,
return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler); OAuth2TokenApi oauth2TokenApi) {
return new TokenAuthenticationFilter(securityProperties, globalExceptionHandler, oauth2TokenApi);
} }
/** /**

View File

@ -1,5 +1,6 @@
package cn.iocoder.yudao.framework.security.core.filter; package cn.iocoder.yudao.framework.security.core.filter;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil; import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils; import cn.iocoder.yudao.framework.common.util.servlet.ServletUtils;
@ -8,7 +9,10 @@ import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler; import cn.iocoder.yudao.framework.web.core.handler.GlobalExceptionHandler;
import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils; import cn.iocoder.yudao.framework.web.core.util.WebFrameworkUtils;
import cn.iocoder.yudao.module.system.api.auth.OAuth2TokenApi;
import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain; import javax.servlet.FilterChain;
@ -30,6 +34,8 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final GlobalExceptionHandler globalExceptionHandler; private final GlobalExceptionHandler globalExceptionHandler;
private final OAuth2TokenApi oauth2TokenApi;
@Override @Override
@SuppressWarnings("NullableProblems") @SuppressWarnings("NullableProblems")
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
@ -39,11 +45,21 @@ public class TokenAuthenticationFilter extends OncePerRequestFilter {
Integer userType = WebFrameworkUtils.getLoginUserType(request); Integer userType = WebFrameworkUtils.getLoginUserType(request);
try { try {
// 验证 token 有效性 // 验证 token 有效性
LoginUser loginUser = null; // TODO 芋艿:待实现 OAuth2AccessTokenCheckRespDTO accessToken = oauth2TokenApi.checkAccessToken(token);
if (accessToken != null && ObjectUtil.notEqual(accessToken.getUserType(), userType)) { // 用户类型不匹配,无权限
throw new AccessDeniedException("错误的用户类型");
}
LoginUser loginUser = null;
if (accessToken != null) { // 如果不为空,说明认证通过,则转换成登录用户
loginUser = new LoginUser().setId(accessToken.getUserId()).setUserType(accessToken.getUserType())
.setTenantId(accessToken.getTenantId());
}
// 模拟 Login 功能,方便日常开发调试 // 模拟 Login 功能,方便日常开发调试
if (loginUser == null) { if (loginUser == null) {
loginUser = mockLoginUser(request, token, userType); loginUser = mockLoginUser(request, token, userType);
} }
// 设置当前用户 // 设置当前用户
if (loginUser != null) { if (loginUser != null) {
SecurityFrameworkUtils.setLoginUser(loginUser, request); SecurityFrameworkUtils.setLoginUser(loginUser, request);

View File

@ -1,30 +0,0 @@
package cn.iocoder.yudao.framework.security.core.service;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import org.springframework.security.core.userdetails.UserDetailsService;
/**
* Security Auth Service {@link UserTypeEnum}
*
* @author
*/
public interface SecurityAuthFrameworkService extends UserDetailsService {
/**
* token
* token
*
* @param token token
* @return
*/
LoginUser verifyTokenAndRefresh(String token);
/**
* SecurityAuthFrameworkService
*
* @return
*/
UserTypeEnum getUserType();
}

View File

@ -84,8 +84,8 @@ public class WebFrameworkUtils {
} }
// 1. 优先,从 Attribute 中获取 // 1. 优先,从 Attribute 中获取
Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE); Integer userType = (Integer) request.getAttribute(REQUEST_ATTRIBUTE_LOGIN_USER_TYPE);
if (userType == null) { if (userType != null) {
return null; return userType;
} }
// 2. 其次,基于 URL 前缀的约定 // 2. 其次,基于 URL 前缀的约定
if (request.getRequestURI().startsWith(properties.getAdminApi().getPrefix())) { if (request.getRequestURI().startsWith(properties.getAdminApi().getPrefix())) {

View File

@ -29,13 +29,6 @@
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<!-- 用户信息 -->
<dependency>
<groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-spring-boot-starter-security</artifactId>
<optional>true</optional>
</dependency>
</dependencies> </dependencies>
</project> </project>

View File

@ -1,12 +0,0 @@
package cn.iocoder.yudao.module.system.api.auth;
/**
* OAuth2.0 API
*
* @author
*/
public interface OAuth2Api {
}

View File

@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.system.api.auth;
import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO;
import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCreateReqDTO;
import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO;
import javax.validation.Valid;
/**
* OAuth2.0 Token API
*
* @author
*/
public interface OAuth2TokenApi {
OAuth2AccessTokenRespDTO createAccessToken(@Valid OAuth2AccessTokenCreateReqDTO reqDTO);
OAuth2AccessTokenCheckRespDTO checkAccessToken(String accessToken);
// void removeToken(OAuth2RemoveTokenByUserReqDTO removeTokenDTO);
}

View File

@ -1,56 +0,0 @@
package cn.iocoder.yudao.module.system.api.auth;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
/**
* 线 Session API
*
* @author
*/
public interface UserSessionApi {
/**
* 线 Session
*
* @param loginUser
* @param userIp IP
* @param userAgent UA
* @return Token
*/
String createUserSession(@NotNull(message = "登录用户不能为空") LoginUser loginUser, String userIp, String userAgent);
/**
* 线 Session
*
* @param token Token
* @param loginUser
*/
void refreshUserSession(@NotEmpty(message = "Token 令牌不能为空") String token,
@NotNull(message = "登录用户不能为空") LoginUser loginUser);
/**
* 线 Session
*
* @param token Token
*/
void deleteUserSession(String token);
/**
* Token 线
*
* @param token Token
* @return 线
*/
LoginUser getLoginUser(String token);
/**
* Session
*
* @return
*/
Long getSessionTimeoutMillis();
}

View File

@ -0,0 +1,28 @@
package cn.iocoder.yudao.module.system.api.auth.dto;
import lombok.Data;
import java.io.Serializable;
/**
* OAuth2.0 访 Response DTO
*
* @author
*/
@Data
public class OAuth2AccessTokenCheckRespDTO implements Serializable {
/**
*
*/
private Long userId;
/**
*
*/
private Integer userType;
/**
*
*/
private Long tenantId;
}

View File

@ -0,0 +1,37 @@
package cn.iocoder.yudao.module.system.api.auth.dto;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.common.validation.InEnum;
import lombok.Data;
import lombok.experimental.Accessors;
import javax.validation.constraints.NotNull;
import java.io.Serializable;
/**
* OAuth2.0 访 Request DTO
*
* @author
*/
@Data
@Accessors(chain = true)
public class OAuth2AccessTokenCreateReqDTO implements Serializable {
/**
*
*/
@NotNull(message = "用户编号不能为空")
private Integer userId;
/**
*
*/
@NotNull(message = "用户类型不能为空")
@InEnum(value = UserTypeEnum.class, message = "用户类型必须是 {value}")
private Integer userType;
/**
*
*/
@NotNull(message = "客户端编号不能为空")
private Long clientId;
}

View File

@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.system.api.auth.dto;
import lombok.Data;
import lombok.experimental.Accessors;
import java.io.Serializable;
import java.util.Date;
/**
* OAuth2.0 访 Response DTO
*
* @author
*/
@Data
@Accessors(chain = true)
public class OAuth2AccessTokenRespDTO implements Serializable {
/**
* 访
*/
private String accessToken;
/**
*
*/
private String refreshToken;
/**
*
*/
private Integer userId;
/**
*
*/
private Integer userType;
/**
*
*/
private Date expiresTime;
}

View File

@ -0,0 +1,33 @@
package cn.iocoder.yudao.module.system.api.auth;
import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO;
import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCreateReqDTO;
import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenRespDTO;
import cn.iocoder.yudao.module.system.convert.auth.UserSessionConvert;
import cn.iocoder.yudao.module.system.service.auth.OAuth2TokenService;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
/**
* OAuth2.0 Token API
*
* @author
*/
@Service
public class OAuth2TokenApiImpl implements OAuth2TokenApi {
@Resource
private OAuth2TokenService oauth2TokenService;
@Override
public OAuth2AccessTokenRespDTO createAccessToken(OAuth2AccessTokenCreateReqDTO reqDTO) {
return null;
}
@Override
public OAuth2AccessTokenCheckRespDTO checkAccessToken(String accessToken) {
return UserSessionConvert.INSTANCE.convert(oauth2TokenService.checkAccessToken(accessToken));
}
}

View File

@ -1,47 +0,0 @@
package cn.iocoder.yudao.module.system.api.auth;
import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.module.system.service.auth.UserSessionService;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
/**
* 线 Session API
*
* @author
*/
@Service
@Validated
public class UserSessionApiImpl implements UserSessionApi {
@Resource
private UserSessionService userSessionService;
@Override
public String createUserSession(LoginUser loginUser, String userIp, String userAgent) {
return userSessionService.createUserSession(loginUser, userIp, userAgent);
}
@Override
public void refreshUserSession(String token, LoginUser loginUser) {
userSessionService.refreshUserSession(token, loginUser);
}
@Override
public void deleteUserSession(String token) {
userSessionService.deleteUserSession(token);
}
@Override
public LoginUser getLoginUser(String token) {
return userSessionService.getLoginUser(token);
}
@Override
public Long getSessionTimeoutMillis() {
return userSessionService.getSessionTimeoutMillis();
}
}

View File

@ -1,6 +1,8 @@
package cn.iocoder.yudao.module.system.convert.auth; package cn.iocoder.yudao.module.system.convert.auth;
import cn.iocoder.yudao.module.system.api.auth.dto.OAuth2AccessTokenCheckRespDTO;
import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageItemRespVO; import cn.iocoder.yudao.module.system.controller.admin.auth.vo.session.UserSessionPageItemRespVO;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO; import cn.iocoder.yudao.module.system.dal.dataobject.auth.UserSessionDO;
import org.mapstruct.Mapper; import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers; import org.mapstruct.factory.Mappers;
@ -12,4 +14,6 @@ public interface UserSessionConvert {
UserSessionPageItemRespVO convert(UserSessionDO session); UserSessionPageItemRespVO convert(UserSessionDO session);
OAuth2AccessTokenCheckRespDTO convert(OAuth2AccessTokenDO bean);
} }

View File

@ -1,7 +1,8 @@
package cn.iocoder.yudao.module.system.dal.dataobject.auth; package cn.iocoder.yudao.module.system.dal.dataobject.auth;
import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; import cn.iocoder.yudao.framework.common.enums.UserTypeEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.tenant.core.db.TenantBaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -12,22 +13,30 @@ import java.util.Date;
/** /**
* OAuth2 访 DO * OAuth2 访 DO
* *
* 使
* user_nameauthentication
*
* @author * @author
*/ */
@TableName("system_oauth2_access_token") @TableName("system_oauth2_access_token")
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Accessors(chain = true) @Accessors(chain = true)
public class OAuth2AccessTokenDO extends BaseDO { public class OAuth2AccessTokenDO extends TenantBaseDO {
/** /**
* *
*/ */
@TableId
private Long id; private Long id;
/** /**
* 访 * 访
*/ */
private String accessToken; private String accessToken;
/**
*
*/
private String refreshToken;
/** /**
* *
*/ */
@ -39,11 +48,11 @@ public class OAuth2AccessTokenDO extends BaseDO {
*/ */
private Integer userType; private Integer userType;
/** /**
* *
* *
* {@link OAuth2ApplicationDO#getId()} * {@link OAuth2ClientDO#getId()}
*/ */
private Long applicationId; private Long clientId;
/** /**
* *
*/ */

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.dal.dataobject.auth;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName; import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data; import lombok.Data;
import lombok.EqualsAndHashCode; import lombok.EqualsAndHashCode;
@ -12,12 +13,8 @@ import java.util.List;
/** /**
* OAuth2 DO * OAuth2 DO
* *
* 使 Client
* 1. clientId id
* 2. GithubGitee
*
* 使 * 使
* authorized_grant_typesauthoritiesaccess_token_validityrefresh_token_validityadditional_informationautoapproveresource_idsscope * authorized_grant_typesauthoritiesadditional_informationautoapproveresource_idsscope
* *
* @author * @author
*/ */
@ -25,24 +22,19 @@ import java.util.List;
@Data @Data
@EqualsAndHashCode(callSuper = true) @EqualsAndHashCode(callSuper = true)
@Accessors(chain = true) @Accessors(chain = true)
public class OAuth2ApplicationDO extends BaseDO { public class OAuth2ClientDO extends BaseDO {
/**
*
*/
private Long id;
/** /**
* *
*
* SQL Server String 使 Long
*/ */
private String clientId; @TableId
private Long id;
/** /**
* *
*/ */
private String clientSecret; private String secret;
/**
* URI
*/
private List<String> redirectUris;
/** /**
* *
*/ */
@ -61,5 +53,17 @@ public class OAuth2ApplicationDO extends BaseDO {
* {@link CommonStatusEnum} * {@link CommonStatusEnum}
*/ */
private Integer status; private Integer status;
/**
* 访
*/
private Integer accessTokenValiditySeconds;
/**
*
*/
private Integer refreshTokenValiditySeconds;
/**
* URI
*/
private List<String> redirectUris;
} }

View File

@ -39,11 +39,11 @@ public class OAuth2CodeDO extends BaseDO {
*/ */
private Integer userType; private Integer userType;
/** /**
* *
* *
* {@link OAuth2ApplicationDO#getId()} * {@link OAuth2ClientDO#getId()}
*/ */
private Long applicationId; private Long clientId;
/** /**
* *
* *

View File

@ -12,6 +12,7 @@ import java.util.Date;
/** /**
* OAuth2 * OAuth2
* *
* @author
*/ */
@TableName("system_oauth2_refresh_token") @TableName("system_oauth2_refresh_token")
@Data @Data
@ -30,20 +31,22 @@ public class OAuth2RefreshTokenDO extends BaseDO {
/** /**
* *
*/ */
private Integer userId; private Long userId;
/** /**
* *
* *
* {@link UserTypeEnum} * {@link UserTypeEnum}
*/ */
private Integer userType; private Integer userType;
/**
*
*
* {@link OAuth2ClientDO#getId()}
*/
private Long clientId;
/** /**
* *
*/ */
private Date expiresTime; private Date expiresTime;
/**
* IP
*/
private String createIp;
} }

View File

@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.system.dal.mysql.auth;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OAuth2AccessTokenMapper extends BaseMapperX<OAuth2AccessTokenDO> {
default OAuth2AccessTokenDO selectByAccessToken(String accessToken) {
return selectOne(OAuth2AccessTokenDO::getAccessToken, accessToken);
}
// default OAuth2AccessTokenDO selectByUserIdAndUserType(Integer userId, Integer userType) {
// return selectOne(new QueryWrapper<OAuth2AccessTokenDO>()
// .eq("user_id", userId).eq("user_type", userType));
// }
//
// default int deleteByUserIdAndUserType(Integer userId, Integer userType) {
// return delete(new QueryWrapper<OAuth2AccessTokenDO>()
// .eq("user_id", userId).eq("user_type", userType));
// }
//
// default int deleteByRefreshToken(String refreshToken) {
// return delete(new QueryWrapper<OAuth2AccessTokenDO>().eq("refresh_token", refreshToken));
// }
//
// default List<OAuth2AccessTokenDO> selectListByRefreshToken(String refreshToken) {
// return selectList(new QueryWrapper<OAuth2AccessTokenDO>().eq("refresh_token", refreshToken));
// }
}

View File

@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.system.dal.mysql.auth;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2RefreshTokenDO;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface OAuth2RefreshTokenMapper extends BaseMapper<OAuth2RefreshTokenDO> {
default int deleteByUserIdAndUserType(Integer userId, Integer userType) {
return delete(new QueryWrapper<OAuth2RefreshTokenDO>()
.eq("user_id", userId).eq("user_type", userType));
}
}

View File

@ -2,6 +2,7 @@ package cn.iocoder.yudao.module.system.dal.redis;
import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine; import cn.iocoder.yudao.framework.redis.core.RedisKeyDefine;
import cn.iocoder.yudao.framework.security.core.LoginUser; import cn.iocoder.yudao.framework.security.core.LoginUser;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
import java.time.Duration; import java.time.Duration;
@ -22,6 +23,10 @@ public interface RedisKeyConstants {
"login_user:%s", // 参数为 token 令牌 "login_user:%s", // 参数为 token 令牌
STRING, LoginUser.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC); STRING, LoginUser.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
RedisKeyDefine OAUTH2_ACCESS_TOKEN = new RedisKeyDefine("访问令牌的缓存",
"oauth2_access_token:%s", // 参数为访问令牌 token
STRING, OAuth2AccessTokenDO.class, RedisKeyDefine.TimeoutTypeEnum.DYNAMIC);
RedisKeyDefine SOCIAL_AUTH_STATE = new RedisKeyDefine("社交登陆的 state", // 注意,它是被 JustAuth 的 justauth.type.prefix 使用到 RedisKeyDefine SOCIAL_AUTH_STATE = new RedisKeyDefine("社交登陆的 state", // 注意,它是被 JustAuth 的 justauth.type.prefix 使用到
"social_auth_state:%s", // 参数为 state "social_auth_state:%s", // 参数为 state
STRING, String.class, Duration.ofHours(24)); // 值为 state STRING, String.class, Duration.ofHours(24)); // 值为 state

View File

@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.system.dal.redis.auth;
import cn.iocoder.yudao.framework.common.util.json.JsonUtils;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Repository;
import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;
import static cn.iocoder.yudao.module.system.dal.redis.RedisKeyConstants.OAUTH2_ACCESS_TOKEN;
/**
* {@link OAuth2AccessTokenDO} RedisDAO
*
* @author
*/
@Repository
public class OAuth2AccessTokenRedisDAO {
@Resource
private StringRedisTemplate stringRedisTemplate;
public OAuth2AccessTokenDO get(String accessToken) {
String redisKey = formatKey(accessToken);
return JsonUtils.parseObject(stringRedisTemplate.opsForValue().get(redisKey), OAuth2AccessTokenDO.class);
}
public void set(OAuth2AccessTokenDO accessTokenDO) {
String redisKey = formatKey(accessTokenDO.getAccessToken());
// 清理多余字段,避免缓存
accessTokenDO.setUpdater(null).setUpdateTime(null).setCreateTime(null).setCreator(null).setDeleted(null);
stringRedisTemplate.opsForValue().set(redisKey, JsonUtils.toJsonString(accessTokenDO),
accessTokenDO.getExpiresTime().getTime() - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
public void delete(String accessToken) {
String redisKey = formatKey(accessToken);
stringRedisTemplate.delete(redisKey);
}
private static String formatKey(String accessToken) {
return String.format(OAUTH2_ACCESS_TOKEN.getKeyTemplate(), accessToken);
}
}

View File

@ -49,6 +49,8 @@ public class AdminAuthServiceImpl implements AdminAuthService {
@Resource @Resource
private UserSessionService userSessionService; private UserSessionService userSessionService;
@Resource @Resource
private OAuth2TokenService oauth2TokenService;
@Resource
private SocialUserService socialUserService; private SocialUserService socialUserService;
@Resource @Resource
@ -207,8 +209,12 @@ public class AdminAuthServiceImpl implements AdminAuthService {
LoginLogTypeEnum logType, String userIp, String userAgent) { LoginLogTypeEnum logType, String userIp, String userAgent) {
// 插入登陆日志 // 插入登陆日志
createLoginLog(loginUser.getId(), username, logType, LoginResultEnum.SUCCESS); createLoginLog(loginUser.getId(), username, logType, LoginResultEnum.SUCCESS);
// 缓存登录用户到 Redis 中,返回 Token 令牌 // 创建访问令牌
return userSessionService.createUserSession(loginUser, userIp, userAgent); // TODO userIp、userAgent
// TODO clientId
return oauth2TokenService.createAccessToken(loginUser.getId(), getUserType().getValue(), 1L)
.getAccessToken();
// return userSessionService.createUserSession(loginUser, userIp, userAgent);
} }
@Override @Override

View File

@ -0,0 +1,22 @@
package cn.iocoder.yudao.module.system.service.auth;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
/**
* OAuth2.0 Client Service
*
* JdbcClientDetailsService
*
* @author
*/
public interface OAuth2ClientService {
/**
*
*
* @param id
* @return
*/
OAuth2ClientDO validOAuthClientFromCache(Long id);
}

View File

@ -0,0 +1,21 @@
package cn.iocoder.yudao.module.system.service.auth;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
import org.springframework.stereotype.Service;
/**
* OAuth2.0 Client Service
*
* @author
*/
@Service
public class OAuth2ClientServiceImpl implements OAuth2ClientService {
@Override
public OAuth2ClientDO validOAuthClientFromCache(Long id) {
return new OAuth2ClientDO().setId(id)
.setAccessTokenValiditySeconds(60 * 30)
.setRefreshTokenValiditySeconds(60 * 60 * 24 * 30);
}
}

View File

@ -1,145 +0,0 @@
package cn.iocoder.yudao.module.system.service.auth;
import org.springframework.stereotype.Service;
/**
* OAuth2.0 Service
*
*
*
* @author
*/
@Service
public class OAuth2ServiceImpl implements OAuth2TokenService {
// @Autowired
// private SystemBizProperties systemBizProperties;
//
// @Autowired
// private OAuth2AccessTokenMapper oauth2AccessTokenMapper;
// @Autowired
// private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper;
//
// @Autowired
// private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO;
//
// @Override
// @Transactional
// public OAuth2AccessTokenRespDTO createAccessToken(OAuth2CreateAccessTokenReqDTO createAccessTokenDTO) {
// // 创建刷新令牌 + 访问令牌
// OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(createAccessTokenDTO.getUserId(),
// createAccessTokenDTO.getUserType(), createAccessTokenDTO.getCreateIp());
// OAuth2AccessTokenDO accessTokenDO = createOAuth2AccessToken(refreshTokenDO, createAccessTokenDTO.getCreateIp());
// // 返回访问令牌
// return OAuth2Convert.INSTANCE.convert(accessTokenDO);
// }
//
// @Override
// @Transactional
// public OAuth2AccessTokenRespDTO checkAccessToken(String accessToken) {
// OAuth2AccessTokenDO accessTokenDO = this.getOAuth2AccessToken(accessToken);
// if (accessTokenDO == null) { // 不存在
// throw ServiceExceptionUtil.exception(OAUTH2_ACCESS_TOKEN_NOT_FOUND);
// }
// if (accessTokenDO.getExpiresTime().getTime() < System.currentTimeMillis()) { // 已过期
// throw ServiceExceptionUtil.exception(OAUTH2_ACCESS_TOKEN_TOKEN_EXPIRED);
// }
// // 返回访问令牌
// return OAuth2Convert.INSTANCE.convert(accessTokenDO);
// }
//
// @Override
// @Transactional
// public OAuth2AccessTokenRespDTO refreshAccessToken(OAuth2RefreshAccessTokenReqDTO refreshAccessTokenDTO) {
// OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectById(refreshAccessTokenDTO.getRefreshToken());
// // 校验刷新令牌是否合法
// if (refreshTokenDO == null) { // 不存在
// throw ServiceExceptionUtil.exception(OAUTH2_REFRESH_TOKEN_NOT_FOUND);
// }
// if (refreshTokenDO.getExpiresTime().getTime() < System.currentTimeMillis()) { // 已过期
// throw ServiceExceptionUtil.exception(OAUTH_REFRESH_TOKEN_EXPIRED);
// }
//
// // 标记 refreshToken 对应的 accessToken 都不合法
// // 这块的实现,参考了 Spring Security OAuth2 的代码
// List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshAccessTokenDTO.getRefreshToken());
// accessTokenDOs.forEach(accessTokenDO -> deleteOAuth2AccessToken(accessTokenDO.getId()));
//
// // 创建访问令牌
// OAuth2AccessTokenDO oauth2AccessTokenDO = createOAuth2AccessToken(refreshTokenDO, refreshAccessTokenDTO.getCreateIp());
// // 返回访问令牌
// return OAuth2Convert.INSTANCE.convert(oauth2AccessTokenDO);
// }
//
// @Override
// @Transactional
// public void removeToken(OAuth2RemoveTokenByUserReqDTO removeTokenDTO) {
// // 删除 Access Token
// OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByUserIdAndUserType(
// removeTokenDTO.getUserId(), removeTokenDTO.getUserType());
// if (accessTokenDO != null) {
// this.deleteOAuth2AccessToken(accessTokenDO.getId());
// }
//
// // 删除 Refresh Token
// oauth2RefreshTokenMapper.deleteByUserIdAndUserType(removeTokenDTO.getUserId(), removeTokenDTO.getUserType());
// }
//
// private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, String createIp) {
// OAuth2AccessTokenDO accessToken = new OAuth2AccessTokenDO()
// .setId(generateAccessToken())
// .setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType())
// .setRefreshToken(refreshTokenDO.getId())
// .setExpiresTime(new Date(System.currentTimeMillis() + systemBizProperties.getAccessTokenExpireTimeMillis()))
// .setCreateIp(createIp);
// oauth2AccessTokenMapper.insert(accessToken);
// return accessToken;
// }
//
// private OAuth2RefreshTokenDO createOAuth2RefreshToken(Integer userId, Integer userType, String createIp) {
// OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO()
// .setId(generateRefreshToken())
// .setUserId(userId).setUserType(userType)
// .setExpiresTime(new Date(System.currentTimeMillis() + systemBizProperties.getRefreshTokenExpireTimeMillis()))
// .setCreateIp(createIp);
// oauth2RefreshTokenMapper.insert(refreshToken);
// return refreshToken;
// }
//
// private OAuth2AccessTokenDO getOAuth2AccessToken(String accessToken) {
// // 优先从 Redis 中获取
// OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken);
// if (accessTokenDO != null) {
// return accessTokenDO;
// }
//
// // 获取不到,从 MySQL 中获取
// accessTokenDO = oauth2AccessTokenMapper.selectById(accessToken);
// // 如果在 MySQL 存在,则往 Redis 中写入
// if (accessTokenDO != null) {
// oauth2AccessTokenRedisDAO.set(accessTokenDO);
// }
// return accessTokenDO;
// }
//
// /**
// * 删除 accessToken 的 MySQL 与 Redis 的数据
// *
// * @param accessToken 访问令牌
// */
// private void deleteOAuth2AccessToken(String accessToken) {
// // 删除 MySQL
// oauth2AccessTokenMapper.deleteById(accessToken);
// // 删除 Redis
// oauth2AccessTokenRedisDAO.delete(accessToken);
// }
//
// private static String generateAccessToken() {
// return StringUtils.uuid(true);
// }
//
// private static String generateRefreshToken() {
// return StringUtils.uuid(true);
// }
}

View File

@ -1,20 +1,66 @@
package cn.iocoder.yudao.module.system.service.auth; package cn.iocoder.yudao.module.system.service.auth;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
/** /**
* OAuth2.0 Token Service * OAuth2.0 Token Service
* *
* Spring Security OAuth JdbcTokenStore 访 * Spring Security OAuth DefaultTokenServices + JdbcTokenStore 访
* *
* @author * @author
*/ */
public interface OAuth2TokenService { public interface OAuth2TokenService {
// OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, String createIp); /**
// * 访
// OAuth2AccessTokenRespDTO checkAccessToken(String accessToken); *
// *
// OAuth2AccessTokenRespDTO refreshAccessToken(OAuth2RefreshAccessTokenReqDTO refreshAccessTokenDTO); * DefaultTokenServices createAccessToken
// *
// void removeToken(OAuth2RemoveTokenByUserReqDTO removeTokenDTO); * @param userId
* @param userType
* @param clientId
* @return 访
*/
OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, Long clientId);
/**
* 访
*
* DefaultTokenServices refreshAccessToken
*
* @param refreshToken
* @return 访
*/
OAuth2AccessTokenDO refreshAccessToken(String refreshToken);
/**
* 访
*
* DefaultTokenServices getAccessToken
*
* @param accessToken 访
* @return 访
*/
OAuth2AccessTokenDO getAccessToken(String accessToken);
/**
* 访
*
* @param accessToken 访
* @return 访
*/
OAuth2AccessTokenDO checkAccessToken(String accessToken);
/**
* 访
*
*
* DefaultTokenServices revokeToken
*
* @param accessToken
* @return
*/
boolean removeAccessToken(String accessToken);
} }

View File

@ -0,0 +1,182 @@
package cn.iocoder.yudao.module.system.service.auth;
import cn.hutool.core.util.IdUtil;
import cn.iocoder.yudao.framework.common.exception.enums.GlobalErrorCodeConstants;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2ClientDO;
import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2RefreshTokenDO;
import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2AccessTokenMapper;
import cn.iocoder.yudao.module.system.dal.mysql.auth.OAuth2RefreshTokenMapper;
import cn.iocoder.yudao.module.system.dal.redis.auth.OAuth2AccessTokenRedisDAO;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.util.Calendar;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
/**
* OAuth2.0 Token Service
*
* @author
*/
@Service
public class OAuth2TokenServiceImpl implements OAuth2TokenService {
@Resource
private OAuth2AccessTokenMapper oauth2AccessTokenMapper;
@Resource
private OAuth2RefreshTokenMapper oauth2RefreshTokenMapper;
@Resource
private OAuth2AccessTokenRedisDAO oauth2AccessTokenRedisDAO;
@Resource
private OAuth2ClientService oauth2ClientService;
@Override
@Transactional
public OAuth2AccessTokenDO createAccessToken(Long userId, Integer userType, Long clientId) {
OAuth2ClientDO clientDO = oauth2ClientService.validOAuthClientFromCache(clientId);
// 创建刷新令牌
OAuth2RefreshTokenDO refreshTokenDO = createOAuth2RefreshToken(userId, userType, clientDO);
// 创建访问令牌
OAuth2AccessTokenDO accessTokenDO = createOAuth2AccessToken(refreshTokenDO, clientDO);
// 记录到 Redis 中
oauth2AccessTokenRedisDAO.set(accessTokenDO);
return accessTokenDO;
}
@Override
public OAuth2AccessTokenDO refreshAccessToken(String refreshToken) {
return null;
}
@Override
public OAuth2AccessTokenDO getAccessToken(String accessToken) {
// 优先从 Redis 中获取
OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenRedisDAO.get(accessToken);
if (accessTokenDO != null) {
return accessTokenDO;
}
// 获取不到,从 MySQL 中获取
accessTokenDO = oauth2AccessTokenMapper.selectById(accessToken);
// 如果在 MySQL 存在,则往 Redis 中写入
if (accessTokenDO != null && !DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
oauth2AccessTokenRedisDAO.set(accessTokenDO);
}
return accessTokenDO;
}
@Override
public OAuth2AccessTokenDO checkAccessToken(String accessToken) {
OAuth2AccessTokenDO accessTokenDO = getAccessToken(accessToken);
if (accessTokenDO == null) {
throw exception(GlobalErrorCodeConstants.UNAUTHORIZED, "Token 不存在");
}
if (DateUtils.isExpired(accessTokenDO.getExpiresTime())) {
throw exception(GlobalErrorCodeConstants.UNAUTHORIZED, "Token 已过期");
}
return accessTokenDO;
}
@Override
public boolean removeAccessToken(String accessToken) {
return false;
}
// @Override
// @Transactional
// public OAuth2AccessTokenRespDTO checkAccessToken(String accessToken) {
// OAuth2AccessTokenDO accessTokenDO = this.getOAuth2AccessToken(accessToken);
// if (accessTokenDO == null) { // 不存在
// throw ServiceExceptionUtil.exception(OAUTH2_ACCESS_TOKEN_NOT_FOUND);
// }
// if (accessTokenDO.getExpiresTime().getTime() < System.currentTimeMillis()) { // 已过期
// throw ServiceExceptionUtil.exception(OAUTH2_ACCESS_TOKEN_TOKEN_EXPIRED);
// }
// // 返回访问令牌
// return OAuth2Convert.INSTANCE.convert(accessTokenDO);
// }
// @Override
// @Transactional
// public OAuth2AccessTokenRespDTO refreshAccessToken(OAuth2RefreshAccessTokenReqDTO refreshAccessTokenDTO) {
// OAuth2RefreshTokenDO refreshTokenDO = oauth2RefreshTokenMapper.selectById(refreshAccessTokenDTO.getRefreshToken());
// // 校验刷新令牌是否合法
// if (refreshTokenDO == null) { // 不存在
// throw ServiceExceptionUtil.exception(OAUTH2_REFRESH_TOKEN_NOT_FOUND);
// }
// if (refreshTokenDO.getExpiresTime().getTime() < System.currentTimeMillis()) { // 已过期
// throw ServiceExceptionUtil.exception(OAUTH_REFRESH_TOKEN_EXPIRED);
// }
//
// // 标记 refreshToken 对应的 accessToken 都不合法
// // 这块的实现,参考了 Spring Security OAuth2 的代码
// List<OAuth2AccessTokenDO> accessTokenDOs = oauth2AccessTokenMapper.selectListByRefreshToken(refreshAccessTokenDTO.getRefreshToken());
// accessTokenDOs.forEach(accessTokenDO -> deleteOAuth2AccessToken(accessTokenDO.getId()));
//
// // 创建访问令牌
// OAuth2AccessTokenDO oauth2AccessTokenDO = createOAuth2AccessToken(refreshTokenDO, refreshAccessTokenDTO.getCreateIp());
// // 返回访问令牌
// return OAuth2Convert.INSTANCE.convert(oauth2AccessTokenDO);
// }
//
// @Override
// @Transactional
// public void removeToken(OAuth2RemoveTokenByUserReqDTO removeTokenDTO) {
// // 删除 Access Token
// OAuth2AccessTokenDO accessTokenDO = oauth2AccessTokenMapper.selectByUserIdAndUserType(
// removeTokenDTO.getUserId(), removeTokenDTO.getUserType());
// if (accessTokenDO != null) {
// this.deleteOAuth2AccessToken(accessTokenDO.getId());
// }
//
// // 删除 Refresh Token
// oauth2RefreshTokenMapper.deleteByUserIdAndUserType(removeTokenDTO.getUserId(), removeTokenDTO.getUserType());
// }
private OAuth2AccessTokenDO createOAuth2AccessToken(OAuth2RefreshTokenDO refreshTokenDO, OAuth2ClientDO clientDO) {
OAuth2AccessTokenDO accessToken = new OAuth2AccessTokenDO().setAccessToken(generateAccessToken())
.setUserId(refreshTokenDO.getUserId()).setUserType(refreshTokenDO.getUserType()).setClientId(clientDO.getId())
.setRefreshToken(refreshTokenDO.getRefreshToken())
.setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getAccessTokenValiditySeconds()));
accessToken.setTenantId(TenantContextHolder.getTenantId()); // 手动设置租户编号,避免缓存到 Redis 的时候,无对应的租户编号
oauth2AccessTokenMapper.insert(accessToken);
return accessToken;
}
private OAuth2RefreshTokenDO createOAuth2RefreshToken(Long userId, Integer userType, OAuth2ClientDO clientDO) {
OAuth2RefreshTokenDO refreshToken = new OAuth2RefreshTokenDO().setRefreshToken(generateRefreshToken())
.setUserId(userId).setUserType(userType).setClientId(clientDO.getId())
.setExpiresTime(DateUtils.addDate(Calendar.SECOND, clientDO.getRefreshTokenValiditySeconds()));
oauth2RefreshTokenMapper.insert(refreshToken);
return refreshToken;
}
// /**
// * 删除 accessToken 的 MySQL 与 Redis 的数据
// *
// * @param accessToken 访问令牌
// */
// private void deleteOAuth2AccessToken(String accessToken) {
// // 删除 MySQL
// oauth2AccessTokenMapper.deleteById(accessToken);
// // 删除 Redis
// oauth2AccessTokenRedisDAO.delete(accessToken);
// }
//
private static String generateAccessToken() {
return IdUtil.fastSimpleUUID();
}
private static String generateRefreshToken() {
return IdUtil.fastSimpleUUID();
}
}

View File

@ -21,11 +21,11 @@
<url>https://github.com/YunaiV/ruoyi-vue-pro</url> <url>https://github.com/YunaiV/ruoyi-vue-pro</url>
<dependencies> <dependencies>
<dependency> <!-- <dependency>-->
<groupId>cn.iocoder.boot</groupId> <!-- <groupId>cn.iocoder.boot</groupId>-->
<artifactId>yudao-module-member-biz</artifactId> <!-- <artifactId>yudao-module-member-biz</artifactId>-->
<version>${revision}</version> <!-- <version>${revision}</version>-->
</dependency> <!-- </dependency>-->
<dependency> <dependency>
<groupId>cn.iocoder.boot</groupId> <groupId>cn.iocoder.boot</groupId>
<artifactId>yudao-module-system-biz</artifactId> <artifactId>yudao-module-system-biz</artifactId>