No5.由内存存储改为数据库存储和redis存储(主要是授权服务端的用户信息、客户端信息、客户授权信息;资源端是没有存储的,每次解析token拿到用户信息);
代码地址与接口看总目录:【学习笔记】记录冷冷-pig项目的学习过程,大概包括Authorization Server、springcloud、Mybatis Plus~~~_清晨敲代码的博客-CSDN博客
前几篇的写的授权端和资源端中,里面的用户登录信息、客户端信息、客户授权信息的存储一直是使用的Spring提供的基于内存的存储,也就是,这种存储方式是不会持久化信息的,是每次重启系统都会重新加载我们写好的信息。
现在将用户登录信息、客户端信息的存储改为持久化的MySQL数据库,将客户授权信息的存储修改为Redis。
我们使用mybatisplus操作mysql数据库,使用springboot的redis来操作Redis。
目录
C2.AuthorizationServerConfiguration
C2.SysOauthClientDetailsMapper
C3.SysOauthClientDetailsServiceImpl
C4.PigRegisteredClientRepository
C1.RegisteredClientConfiguration
C1.PigRedisOAuth2AuthorizationServiceImpl
C2.RegisteredClientConfiguration
C3.resources/META-INF/spring.factories(pig-common-core和pig-auth)
C1.PigRegisteredClientRepository
A1.修改用户登录信息的持久化为MySQL
思考一下,这个用户登录信息持久化,只有pigDaoAuthenticationProvider类会涉及到,他需要调用this.userDetailsService.loadUserByUsername()方法来获取用户信息,userDetailsService现在是用的基于内存的InMemoryUserDetailsManager。
我们只需要自定义一个实现userDetailsService接口的类,让他从数据库中拿到数据就可以了!
B1.依赖包
?由于不想直接在auth模块添加数据库相关的,就创建了一个loginUser模块,我看PIg里面是通过微服务调用的umps模块,我们这里就不那样做的,直接给auth依赖loginUser模块调用吧
其中要想使用mybatisplus调用mysql,需要导入下面两个包(mybatis-plus-boot-starter就依赖了jdbc)
<!--orm 相关-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--连接数据库相关-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
B2.核心类
C1.SysUser
和平时添加的表关联实体一样,只是对于主键需要加个注解,这样方便使用mybatisplus提供的通过主键操作的方法。
对于删除标识可以选择添加@TableLogic注解,添加之后会变成逻辑删除,只修改状态而不是真正的删除,并且使用自带的查询是,会自定查询未删除的。
@Data
@EqualsAndHashCode(callSuper = true)
public class SysUser extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 主键ID
*/
@TableId(value = "user_id", type = IdType.ASSIGN_ID)
private Long userId;
。。。
/**
* 0-正常,1-删除
*/
//表示逻辑删除注解
@TableLogic(value="0",delval="1")
private String delFlag;
}
C2.SysUserMapper
mapper接口继承 BaseMapper 类后,就无需编写 mapper.xml 文件,可以直接使用定义好的CRUD功能。
对于注册获取用户来说,只需要获取用户信息就可以,就不用再加mapper.xml文件了
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
@Mapper
public interface SysUserMapper extends BaseMapper<SysUser> {
}
C3.SysUserServiceImpl
业务接口和业务实现类需要分别继承?IService 、ServiceImpl,这样就可以使用mybatisplus提供的操作方法。
我们目前只需要业务类的 getOne(Wrapper)方法即可。
public interface SysUserService extends IService<SysUser> {
}
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
}
C4.PigUserDetailsServiceImpl
这个类是专门处理用户登录时,获取用户信息的,需要实现 UserDetailsService 接口。
在这个类里面使用SysUserServiceImpl拿到用户信息~
这里需要注意,拿到持久化的用户信息后,需要给密码加一个密码器类型:{bcrypt},因为我们在PigDaoAuthenticationProvider里面用的是 DelegatingPasswordEncoder 类型的密码管理器,他需要根据头部密码标识来判断用哪个密码管理器解码。(同时注意,这里加的密码标识和保存密码时的密码标识需要一致哦~)
@Service
@RequiredArgsConstructor
public class PigUserDetailsServiceImpl implements UserDetailsService {
private final SysUserService sysUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getUsername,username));
//这里需要给密码加入密码器类型:{bcrypt},因为我们用的是 DelegatingPasswordEncoder ,需要根据头部判断用哪个密码管理器
PigUser userDetails = new PigUser(sysUser.getUsername(), SecurityConstants.BCRYPT + sysUser.getPassword(),new HashSet(Collections.singleton(new SimpleGrantedAuthority("write"))));
return userDetails;
}
}
B3.配置类
C1.application.xml
# freemark配置
spring:
# 缓存类型使用redis
cache:
type: redis
redis:
host: pig-redis
port: 6379
# 数据源
datasource:
type: com.zaxxer.hikari.HikariDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
username: username
password: password
url: jdbc:mysql://pig-mysql:3306/pig?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowMultiQueries=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Shanghai&nullCatalogMeansCurrent=true&allowPublicKeyRetrieval=true
C2.AuthorizationServerConfiguration
注入一个UserDetailsService类型的bean,这里就会拿到我们自定义的PigUserDetailsServiceImpl类型的对象。
然后将原来的userDetailsService()方法创建Bean去掉,并且修改PigDaoAuthenticationProvider 对象的userDetailsService对象。
/**
* 认证服务器配置
*/
@RequiredArgsConstructor
@Configuration
public class AuthorizationServerConfiguration {
private final UserDetailsService userDetailsService;
/**
* @Description: 注入授权模式实现提供方
* 1. 密码模式 </br>
* 2. 短信登录 (暂无)</br>
* @param http
* @Return: void
*/
public void addCustomOAuth2GrantAuthenticationProvider(HttpSecurity http){
。。。
//new一个自定义用户认证提供方,(类似于DaoAuthenticationProvider)需要注入用户管理业务userDetailsService,当前用的是InMemoryUserDetailsManager,生产模式最好不要使用
PigDaoAuthenticationProvider pigDaoAuthenticationProvider = new PigDaoAuthenticationProvider();
pigDaoAuthenticationProvider.setUserDetailsService(userDetailsService);
。。。
}
// @Bean
// UserDetailsService userDetailsService(){
//
// UserDetails userDetails = User.builder()
// .username("qc")
// .password("123")
// .passwordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder()::encode)
// .roles("read","write")
// .build();
// //new一个用户管理业务,注入一盒用户信息
// return new InMemoryUserDetailsManager(userDetails);
// }
}
C3.WebSecurityConfiguration
注入一个UserDetailsService类型的bean,这里就会拿到我们自定义的PigUserDetailsServiceImpl类型的对象。
修改PigDaoAuthenticationProvider 对象的userDetailsService对象。
@RequiredArgsConstructor
@Configuration(proxyBeanMethods = false)
public class WebSecurityConfiguration {
private final UserDetailsService userDetailsService;
/**
* @Description: spring security 默认的安全策略
* @param http
* @Return: org.springframework.security.web.SecurityFilterChain
*/
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
。。。
PigDaoAuthenticationProvider pigDaoAuthenticationProvider = new PigDaoAuthenticationProvider();
pigDaoAuthenticationProvider.setUserDetailsService(userDetailsService);
。。。
}
}
A2.修改客户信息的持久化为MySQL
思考一下,这个客户端信息(注意和客户端认证信息不一样哦)持久化,只有RegisteredClientRepository类会涉及到,现在是用的基于内存的InMemoryRegisteredClientRepository。
我们只需要自定义一个实现RegisteredClientRepository接口的类,让他从数据库中拿到数据就可以了!
B1.依赖包
和A1.一样的依赖包
B2.核心类
C1.SysOauthClientDetails
@Data
@EqualsAndHashCode(callSuper = true)
public class SysOauthClientDetails extends BaseEntity {
private static final long serialVersionUID = 1L;
/**
* 客户端ID
*/
@TableId(value = "client_id", type = IdType.INPUT)
private String clientId;
/**
* 客户端密钥
*/
private String clientSecret;
/**
* 作用域
*/
private String scope;
/**
* 授权方式(A,B,C)
*/
private String authorizedGrantTypes;
/**
* 回调地址
*/
private String webServerRedirectUri;
/**
* 权限
*/
private String authorities;
/**
* 请求令牌有效时间
*/
private Integer accessTokenValidity;
/**
* 刷新令牌有效时间
*/
private Integer refreshTokenValidity;
/**
* 扩展信息
*/
private String additionalInformation;
/**
* 是否自动放行,也就是是否打开授权确认页面
*/
private String autoapprove;
}
C2.SysOauthClientDetailsMapper
@Mapper
public interface SysOauthClientDetailsMapper extends BaseMapper<SysOauthClientDetails> {
}
C3.SysOauthClientDetailsServiceImpl
public interface SysOauthClientDetailsService extends IService<SysOauthClientDetails> {
}
@Service
public class SysOauthClientDetailsServiceImpl extends ServiceImpl<SysOauthClientDetailsMapper, SysOauthClientDetails>
implements SysOauthClientDetailsService {
}
C4.PigRegisteredClientRepository
先从数据库中拿到客户信息,然后构建成RegisteredClient对象并返回。
@Service
@RequiredArgsConstructor
public class PigRegisteredClientRepository implements RegisteredClientRepository {
/**
* 刷新令牌有效期默认 30 天
*/
private final static int refreshTokenValiditySeconds = 60 * 60 * 24 * 30;
/**
* 请求令牌有效期默认 12 小时
*/
private final static int accessTokenValiditySeconds = 60 * 60 * 12;
private final SysOauthClientDetailsService sysOauthClientDetailsService;
@Override
public void save(RegisteredClient registeredClient) {
throw new UnsupportedOperationException();
}
@Override
public RegisteredClient findById(String s) {
throw new UnsupportedOperationException();
}
@Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null")
@Override
public RegisteredClient findByClientId(String clientId) {
SysOauthClientDetails clientDetails = sysOauthClientDetailsService.getOne(Wrappers.<SysOauthClientDetails>query().lambda().eq(SysOauthClientDetails::getClientId,clientId));
RegisteredClient.Builder builder = RegisteredClient.withId(clientDetails.getClientId())
.clientId(clientDetails.getClientId())
.clientSecret(SecurityConstants.NOOP + clientDetails.getClientSecret())
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC);
// 授权模式
Optional.ofNullable(clientDetails.getAuthorizedGrantTypes())
.ifPresent(grants -> StringUtils.commaDelimitedListToSet(grants).forEach(s -> builder.authorizationGrantType(new AuthorizationGrantType(s))));
// 回调地址
Optional.ofNullable(clientDetails.getWebServerRedirectUri())
.ifPresent(redirectUri -> StringUtils.commaDelimitedListToSet(redirectUri).forEach(r -> builder.redirectUri(r)));
// scope 分割多个scope值
Optional.ofNullable(clientDetails.getScope())
.ifPresent(scopes -> StringUtils.commaDelimitedListToSet(scopes).forEach(s -> builder.scope(s)));
return builder
.tokenSettings(TokenSettings.builder()
.accessTokenFormat(OAuth2TokenFormat.REFERENCE)
.accessTokenTimeToLive(Duration.ofSeconds(Optional.ofNullable(clientDetails.getAccessTokenValidity()).orElse(accessTokenValiditySeconds)))
.refreshTokenTimeToLive(Duration.ofSeconds(Optional.ofNullable(clientDetails.getRefreshTokenValidity()).orElse(refreshTokenValiditySeconds)))
.build())
.clientSettings(ClientSettings.builder()
.requireAuthorizationConsent(!BooleanUtil.toBoolean(clientDetails.getAutoapprove())).build())
.build();
}
}
B3.配置类
C1.RegisteredClientConfiguration
这个类里面配置客户信息的全部去掉,不再需要了。
@RequiredArgsConstructor
@Configuration
public class RegisteredClientConfiguration {
。。。
// public RegisteredClientRepository registeredClientRepository() {。。。
// }
// private RegisteredClient createRegisteredClient_umps(final String id) {。。。
// private RegisteredClient createRegisteredClient_client(final String id) {。。。
// }
}
A3.修改客户授权信息的持久化为Redis
我们直接将redis配置添加到common-core里面。
涉及到客户授权信息的持久化操作是OAuth2AuthorizationService类,我们也实现这个类,然后在他里面调用RedisTemplate操作redis即可。
B1.依赖包
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
B2.核心类
C1.PigRedisOAuth2AuthorizationServiceImpl
代码很好理解,主要是三个方法,一个save一个remove一个findbytoken。
注意findbytoken方法有一个参数是OAuth2TokenType,就是可以根据token和token的类型(code、access_token、refresh_token)来查询,那么我们保存时也要根据这个token类型保存,以防止查询错误。
进入save方法后根据OAuth2Authorization 里面的token类型来进行保存。只要OAuth2Authorization 携带的信息符合条件就会保存~
@RequiredArgsConstructor
public class PigRedisOAuth2AuthorizationServiceImpl implements OAuth2AuthorizationService {
private final static Long TIMEOUT = 10L;
private static final String AUTHORIZATION = "token";
private final RedisTemplate<String, Object> redisTemplate;
@Override
public void save(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
if (isState(authorization)) {
String token = authorization.getAttribute("state");
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.STATE, token), authorization, TIMEOUT,
TimeUnit.MINUTES);
}
if (isCode(authorization)) {
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
.getToken(OAuth2AuthorizationCode.class);
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
long between = ChronoUnit.MINUTES.between(authorizationCodeToken.getIssuedAt(),
authorizationCodeToken.getExpiresAt());
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()),
authorization, between, TimeUnit.MINUTES);
}
if (isRefreshToken(authorization)) {
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
long between = ChronoUnit.SECONDS.between(refreshToken.getIssuedAt(), refreshToken.getExpiresAt());
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()),
authorization, between, TimeUnit.SECONDS);
}
if (isAccessToken(authorization)) {
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()),
authorization, between, TimeUnit.SECONDS);
}
}
@Override
public void remove(OAuth2Authorization authorization) {
Assert.notNull(authorization, "authorization cannot be null");
List<String> keys = new ArrayList<>();
if (isState(authorization)) {
String token = authorization.getAttribute("state");
keys.add(buildKey(OAuth2ParameterNames.STATE, token));
}
if (isCode(authorization)) {
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
.getToken(OAuth2AuthorizationCode.class);
OAuth2AuthorizationCode authorizationCodeToken = authorizationCode.getToken();
keys.add(buildKey(OAuth2ParameterNames.CODE, authorizationCodeToken.getTokenValue()));
}
if (isRefreshToken(authorization)) {
OAuth2RefreshToken refreshToken = authorization.getRefreshToken().getToken();
keys.add(buildKey(OAuth2ParameterNames.REFRESH_TOKEN, refreshToken.getTokenValue()));
}
if (isAccessToken(authorization)) {
OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
keys.add(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()));
}
redisTemplate.delete(keys);
}
@Override
@Nullable
public OAuth2Authorization findById(String id) {
throw new UnsupportedOperationException();
}
@Override
@Nullable
public OAuth2Authorization findByToken(String token, @Nullable OAuth2TokenType tokenType) {
Assert.hasText(token, "token cannot be empty");
Assert.notNull(tokenType, "tokenType cannot be empty");
return (OAuth2Authorization) redisTemplate.opsForValue().get(buildKey(tokenType.getValue(), token));
}
private String buildKey(String type, String id) {
return String.format("%s::%s::%s", AUTHORIZATION, type, id);
}
private static boolean isState(OAuth2Authorization authorization) {
return Objects.nonNull(authorization.getAttribute("state"));
}
private static boolean isCode(OAuth2Authorization authorization) {
OAuth2Authorization.Token<OAuth2AuthorizationCode> authorizationCode = authorization
.getToken(OAuth2AuthorizationCode.class);
return Objects.nonNull(authorizationCode);
}
private static boolean isRefreshToken(OAuth2Authorization authorization) {
return Objects.nonNull(authorization.getRefreshToken());
}
private static boolean isAccessToken(OAuth2Authorization authorization) {
return Objects.nonNull(authorization.getAccessToken());
}
}
B3.配置类
C1.RedisTemplateConfiguration
创建一个RedisTemplate的bean。
这里有点小问题,系统序列化存储信息,拿取信息是正常的,但是在RedisDeskTopManager工具里看到的是乱码,还没找到解决办法o(╥﹏╥)o待解决
@EnableCaching
@AutoConfiguration
@AutoConfigureBefore(RedisAutoConfiguration.class)
public class RedisTemplateConfiguration {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(RedisSerializer.string());
redisTemplate.setHashKeySerializer(RedisSerializer.string());
redisTemplate.setValueSerializer(RedisSerializer.java());
redisTemplate.setHashValueSerializer(RedisSerializer.java());
redisTemplate.setConnectionFactory(factory);
return redisTemplate;
}
@Bean
public HashOperations<String, String, Object> hashOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForHash();
}
@Bean
public ValueOperations<String, String> valueOperations(RedisTemplate<String, String> redisTemplate) {
return redisTemplate.opsForValue();
}
@Bean
public ListOperations<String, Object> listOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForList();
}
@Bean
public SetOperations<String, Object> setOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForSet();
}
@Bean
public ZSetOperations<String, Object> zSetOperations(RedisTemplate<String, Object> redisTemplate) {
return redisTemplate.opsForZSet();
}
}
C2.RegisteredClientConfiguration
这个类里面配置客户授权信息的全部去掉,不再需要了。或者直接删除这个类吧,不再需要了
// @Bean
// public OAuth2AuthorizationService oAuth2AuthorizationService(){
// //new一个授权管理业务,这里用的基于内存持久的
// OAuth2AuthorizationService oAuth2AuthorizationService = new InMemoryOAuth2AuthorizationService();
// return oAuth2AuthorizationService;
// }
//
C3.resources/META-INF/spring.factories(pig-common-core和pig-auth)
在未直接import依赖时,同时这个类又不是启动类的同级及子类,就不会被扫描成bean,所以,我们需要手动import,可以使用@ComponentScans(""),也可以通过spring.factories引入。
--------------pig-auth
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pig4cloud.pig.auth.service.impl.PigRedisOAuth2AuthorizationServiceImpl
--------------pig-common-core
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.pig4cloud.pig.common.core.config.RedisTemplateConfiguration
A4.给用户登录信息和客户信息添加cache缓存
有两种方式使用Cache缓存,一种是直接拿到CacheManager对象,然后拿到指定的Cache并调用get,set;一种是通过注解来调用;
B1.用户登录信息添加cache缓存
C1.PigUserDetailsServiceImpl
@Service
@RequiredArgsConstructor
public class PigUserDetailsServiceImpl implements UserDetailsService {
private final SysUserService sysUserService;
private final CacheManager cacheManager;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Cache cache = cacheManager.getCache(CacheConstants.USER_DETAILS);
if (cache != null && cache.get(username) != null) {
return (PigUser) cache.get(username).get();
}
SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>query().lambda().eq(SysUser::getUsername,username));
//这里需要给密码加入密码器类型:{bcrypt},因为我们用的是 DelegatingPasswordEncoder ,需要根据头部判断用哪个密码管理器
PigUser userDetails = new PigUser(sysUser.getUsername(), SecurityConstants.BCRYPT + sysUser.getPassword(),new HashSet(Collections.singleton(new SimpleGrantedAuthority("write"))));
if (cache != null) {
cache.put(username, userDetails);
}
return userDetails;
}
}
B2.客户信息添加cache缓存
C1.PigRegisteredClientRepository
@Cacheable(value = CacheConstants.CLIENT_DETAILS_KEY, key = "#clientId", unless = "#result == null")
@Override
public RegisteredClient findByClientId(String clientId) {
。。。
}