项目放在gitee
https://gitee.com/hunterhyl/common-techniques4.git 下面的oauth2模块
建议:使用oauth2之前先看一些基本介绍,里面有一个概念还是需要了解的
该项目需要本地启动 mysql和redis
依赖
<properties>
<java.version>11</java.version>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.7.11</spring-boot.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.11</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.7.11</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.7.11</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.28</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.25</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
<version>2.2.4.RELEASE</version>
<exclusions>
<exclusion>
<artifactId>bcpkix-jdk15on</artifactId>
<groupId>org.bouncycastle</groupId>
</exclusion>
<exclusion>
<artifactId>spring-cloud-starter</artifactId>
<groupId>org.springframework.cloud</groupId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
配置文件
spring.application.name=auth-service
server.port=8080
spring.redis.host=localhost
spring.redis.port=6379
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=填写自己的
spring.datasource.url=jdbc:mysql://localhost:3306/填写自己的?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
mybatis-plus.mapper-locations=classpath:mapper/*.xml
项目结构
思路
首先是生成token的入口在哪里,找到入口之口就能够缺啥补啥
tokenEndpoint.postAccessToken()是入口 tokenEndpoint直接@Autowired注入即可
缺少一个 Principal 和一个 Map,点进去看看,第一行就发现其实要的是 Authentication 类,随意说缺的是Authentication+Map
那么就去看一下Authentication 类,其实是个接口,那么 使用 ctrl+H 看一下框架中原本有哪些实现类
实现类很多,那么我的做法是:找到一个实现类,然后复制一份,改成适合自己的项目
我选择的是 UsernamePasswordAuthenticationToken 这个实现类,复制一份改成我自己的 JiuBoDouUsernamePasswordAuthenticationToken 这个实现类,至于有些细节的地方要改什么东西,先不着急
现在第一步已经可以继续执行下去了,new出我们自己的实现类,和一个空的 map 传到 postAccessToken()里面
但是new JiuBoDouUsernamePasswordAuthenticationToken 的时候,我们又发现:JiuBoDouUsernamePasswordAuthenticationToken 有两个构造方法,用哪个呢?
public JiuBoDouUsernamePasswordAuthenticationToken(Object principal, Object credentials) {
super((Collection)null);
this.principal = principal;
this.credentials = credentials;
this.setAuthenticated(false);//这里
}
public JiuBoDouUsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);//这里
}
这两个方法还是很明显不一样的,我们选择参数多的这个,那么Collection<? extends GrantedAuthority> authorities该怎么写,还是先不着急,传递一个空的进去就行
public class JiuBoDouUsernamePasswordAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = 570L;
private final Object principal;//这个是什么意思
private Object credentials;//这个又是什么意思
在构造方法的时候,这两个值是需要传的,那么传什么呢? 先理解成 principal就是用户的登录手机号,或者说是唯一标识 credentials就是用户输入的密码
于是就有了这样的代码:
@GetMapping("/login/{username}/{password}")
public String login(@PathVariable("username") String username, @PathVariable("password") String password) throws HttpRequestMethodNotSupportedException {
Map<String, String> map = new HashMap<>();
map.put("client_id", "jiubodou_client_id");//map里面放的数据是后面debug的时候发现需要这些参数,又回到这里补上去的,正常来说按照我上面的思路这里的map其实是空的,什么数据都没有的
map.put("grant_type", "jiubodou_grant_type");
map.put("username", username);
map.put("password", password);
JiuBoDouUsernamePasswordAuthenticationToken authenticationToken =
new JiuBoDouUsernamePasswordAuthenticationToken(username, password,
new ArrayList<SimpleGrantedAuthority>());
ResponseEntity<OAuth2AccessToken> postedAccessToken = tokenEndpoint.postAccessToken(authenticationToken,
map);
return Objects.requireNonNull(postedAccessToken.getBody()).getValue();
}
好了,现在继续debug
这一行,进去看看
发现这里如果不使用三个参数的构造方法的话,这里就会抛错,所以我们不能用两个参数的构造方法,继续,看这里
这里的 client.getName方法是走的JiuBoDouUsernamePasswordAuthenticationToken父类的方法,如下:
public String getName() {
if (this.getPrincipal() instanceof UserDetails) {
return ((UserDetails)this.getPrincipal()).getUsername();
} else if (this.getPrincipal() instanceof AuthenticatedPrincipal) {
return ((AuthenticatedPrincipal)this.getPrincipal()).getName();
} else if (this.getPrincipal() instanceof Principal) {
return ((Principal)this.getPrincipal()).getName();
} else {
return this.getPrincipal() == null ? "" : this.getPrincipal().toString();
}
}
此时如果积继续debug下去,就会发现最终的clientId拿到的是用户用于登录的手机号,那么这肯定是不对的,用户用于登陆的手机号怎么又变成了 clientId呢?所以我的方法是,在JiuBoDouUsernamePasswordAuthenticationToken里面重写 getName方法,如下
@Override
public String getName() {
return "jiubodou_client_id";
}
这样的话就会有如下效果
继续:
见名知意:根据 clientId 加载信息,那么问题就随之而来,去哪里加载?数据库么?如果是数据库的话,我好像没告诉oauth2数据库连接是哪里吧,表又是哪一个吧
如果此时去看 this.getClientDetailsService()的结果会发现,给的是 InMemoryClientDetailsService ,也就是从内存中拿。这显然不是我们希望的,其实去看一眼就知道,oauth2是给我们准备了数据库连接的
就是下面这个 JdbcClientDetailsService 那么如何启用,如何配置?表怎么建? 如下:
@Configuration
public class JiuBoDouOauth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
clients.withClientDetails(jdbcClientDetailsService);
}
}
-- auto-generated definition
create table oauth_client_details
(
client_id varchar(128) not null comment '客户端ID'
primary key,
resource_ids varchar(256) null comment '资源ID集合,多个资源时用英文逗号分隔',
client_secret varchar(256) null comment '客户端密匙',
scope varchar(256) null comment '客户端申请的权限范围',
authorized_grant_types varchar(256) null comment '客户端支持的grant_type',
web_server_redirect_uri varchar(256) null comment '重定向URI',
authorities varchar(256) null comment '客户端所拥有的SpringSecurity的权限值,多个用英文逗号分隔',
access_token_validity int null comment '访问令牌有效时间值(单位秒)',
refresh_token_validity int null comment '更新令牌有效时间值(单位秒)',
additional_information varchar(4096) null comment '预留字段',
autoapprove varchar(256) null comment '用户是否自动Approval操作'
)
comment '客户端信息' charset = utf8mb3
row_format = DYNAMIC;
附赠一条数据
INSERT INTO cloud_order.oauth_client_details (client_id, resource_ids, client_secret, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove) VALUES ('jiubodou_client_id', null, 'cfc428696a9bca6321b629bbcfb8ddd6', 'all', 'jiubodou_grant_type', null, null, 3600, 604800, null, '1');
继续看
这里执行的代码是 DefaultOAuth2RequestFactory 里面的 createTokenRequest 方法
public TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient) {
String clientId = (String)requestParameters.get("client_id");//这里说明最开始的map中需要client_id
if (clientId == null) {
clientId = authenticatedClient.getClientId();
} else if (!clientId.equals(authenticatedClient.getClientId())) {
throw new InvalidClientException("Given client ID does not match authenticated client");
}
String grantType = (String)requestParameters.get("grant_type");//这里说明最开始的map中需要grant_type
Set<String> scopes = this.extractScopes(requestParameters, clientId);
TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);
return tokenRequest;
}
继续:
这里是拿到生成器去生成 token,进去看一下,getTokenGranter()得到的是 CompositeTokenGranter
再去看 CompositeTokenGranter.grant 方法:
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
Iterator var3 = this.tokenGranters.iterator();//可以看出来,是一个 遍历操作 也就是说在CompositeTokenGranter这个类中,有一个tokenGranters集合,里面存放着所有的 tokenGranter,该方法的作用就是挨个去执行每一个tokenGranter里面的grant方法。
OAuth2AccessToken grant;
do {
if (!var3.hasNext()) {
return null;
}
TokenGranter granter = (TokenGranter)var3.next();//可以看出来,是一个 遍历操作 也就是说在CompositeTokenGranter这个类中,有一个tokenGranters集合,里面存放着所有的 tokenGranter,该方法的作用就是挨个去执行每一个tokenGranter里面的grant方法。
grant = granter.grant(grantType, tokenRequest);
} while(grant == null);
return grant;
}
那么继续看下去
这里就是拿到具体的一个 tokenGranter,然后执行grant方法。那么问题显而易见,我们上面自定义了 grant_type=jiubodou_grant_type
。那么问题是这里面有没有 哪一个 granter是为jiubodou_grant_type这个生成类型服务的granter呢?很显然是没有的
我们进到 grant 这个方法里面
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) { //这里就去匹配,当前的这个 granter 是不是用来生成 jiubodou_grant_type的,如果不是,就直接返回null,在此应证了 当前的系统中是没有 granter 来生成我们的token的,那么我们需要自己去建立一个
return null;
} else {
String clientId = tokenRequest.getClientId();
ClientDetails client = this.clientDetailsService.loadClientByClientId(clientId);
this.validateGrantType(grantType, client);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Getting access token for: " + clientId);
}
return this.getAccessToken(client, tokenRequest);
}
}
继续,那么我们应该去建造一个 专门用来生成 jiubodou_grant_type 类型的 granter ,依旧是,找到现成的实现类,然后复制,然后改动,下面是系统中现有的实现类
我直接复制了 ResourceOwnerPasswordTokenGranter当作自己的 JiuBoDouResourceOwnerPasswordTokenGranter。然后改动一些东西,首先看一下 ResourceOwnerPasswordTokenGranter
public class ResourceOwnerPasswordTokenGranter extends AbstractTokenGranter {
private static final String GRANT_TYPE = "password"; //这里需要改吧
private final AuthenticationManager authenticationManager;
public ResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
this(authenticationManager, tokenServices, clientDetailsService, requestFactory, "password");//这里需要改吧
}
protected ResourceOwnerPasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authenticationManager = authenticationManager;
}
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
String username = (String)parameters.get("username");//这里告诉我们 最开始的map中需要放 username
String password = (String)parameters.get("password");//这里告诉我们 最开始的map中需要放 password
parameters.remove("password");
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);//这里需要改吧
((AbstractAuthenticationToken)userAuth).setDetails(parameters);
Authentication userAuth;
try {
userAuth = this.authenticationManager.authenticate(userAuth);
} catch (AccountStatusException var8) {
throw new InvalidGrantException(var8.getMessage());
} catch (BadCredentialsException var9) {
throw new InvalidGrantException(var9.getMessage());
}
if (userAuth != null && userAuth.isAuthenticated()) {
OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
} else {
throw new InvalidGrantException("Could not authenticate user: " + username);
}
}
}
那么改完之后还需要放到 CompositeTokenGranter里面的那个集合里面对吧
@Configuration
public class JiuBoDouOauth2Config extends AuthorizationServerConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private AuthenticationManager authenticationManager;
@Override
/*使用 jdbc 来查询数据库中的 client 信息*/
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService(dataSource);
clients.withClientDetails(jdbcClientDetailsService);
}
@Autowired //是我们自己放到容器中的,看下去 不着急
private JiuBoDouTokenService jiuBoDouTokenService;
/*添加自己的 TokenGranter*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
ArrayList<TokenGranter> tokenGranters = new ArrayList<>();
//看到这个里先别着急,看下面的说明
JiuBoDouResourceOwnerPasswordTokenGranter jiuBoDouResourceOwnerPasswordTokenGranter =
new JiuBoDouResourceOwnerPasswordTokenGranter(
authenticationManager,
jiuBoDouTokenService,
endpoints.getClientDetailsService(),
endpoints.getOAuth2RequestFactory()
);
tokenGranters.add(jiuBoDouResourceOwnerPasswordTokenGranter);
CompositeTokenGranter compositeTokenGranter = new CompositeTokenGranter(tokenGranters);
endpoints.tokenGranter(compositeTokenGranter);
}
}
上面我放的是成形之后的代码,但是一开始不是这样的,当我们有了自己的 jiuBoDouResourceOwnerPasswordTokenGranter 之后,new出来的时候就发现了很多问题,会发现好多入参啊,而且都不认识,怎么办?
不着急,先看第一个入参,AuthenticationManager authenticationManager,在我们之前的描述中完全没听过这个东西,怎么办?还是找一个现成的实现类,复制一份改一改么?这个不用。我们这样就可以了:
@Configuration
public class JiuBoDouWebConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}//这里 实际放到容器中的是一个 WebSecurityConfigurerAdapter
@Override //这段代码就不用详细说了吧
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/oauth2/**").permitAll()
.anyRequest().authenticated();
http.cors();/*允许跨域*/
}
}
直接将框架中自带的一个 AuthenticationManager 放入到容器中,然后在需要的地方直接注入,就解决了第一个参数
继续,看第二个参数:AuthorizationServerTokenServices tokenServices,这个也是没见过的东西,我依旧是找现成的实现类,复制一份,于是就有了 自己的 JiuBoDouTokenService,同样的,也把自己的 JiuBoDouTokenService 创建出来放到容器中
@Configuration
public class TokenServiceConfig {
@Autowired //是我们自己注入到容器中的,看下去不着急
private RedisTokenStore redisTokenStore;
@Bean
public JiuBoDouTokenService jiuBoDouTokenService() {
JiuBoDouTokenService jiuBoDouTokenService = new JiuBoDouTokenService();
jiuBoDouTokenService.setSupportRefreshToken(true);
jiuBoDouTokenService.setTokenStore(redisTokenStore);
return jiuBoDouTokenService;
}
}
而这里的
@Autowired
private RedisTokenStore redisTokenStore;
这么获取:
@Configuration
public class RedisTokenStoreConfig {
@Autowired //这个是能直接注入的,框架中自带的
private RedisConnectionFactory redisConnectionFactory;
@Bean
public RedisTokenStore redisTokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
}
有点绕对不对,那么就倒着看一下, 先有了框架中自带的RedisConnectionFactory 于是有了自己的RedisTokenStore ,
因为有了 RedisTokenStore ,就能有 JiuBoDouTokenService
因为有了JiuBoDouTokenService ,所以第二个参数就解决了
继续,看第三个参数:
ClientDetailsService clientDetailsService 和 OAuth2RequestFactory requestFactory
ClientDetailsService 还记得把,我们之前就用 jdbc 代替了 内存中的查找
而OAuth2RequestFactory 直接使用默认的就行
所以才有了最终的代码
好的,我们重新回到 grant方法,现在 我们自己的 granter也放到了框架中了,终于能跨过
这一步了对吧,继续 debug下去,来到这一步:进去
来到了这里:
我们看 this.getOAuth2Authentication(client, tokenRequest) 这个方法,进到的是我们自己 granter 里面的方法,断点别打错了
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
String username = (String)parameters.get("username");//最开始的map中需要 username
String password = (String)parameters.get("password");//最开始的map中需要 password
parameters.remove("password");
Authentication userAuth = new JiuBoDouUsernamePasswordAuthenticationToken(username, password);//用我们自己的JiuBoDouUsernamePasswordAuthenticationToken,而且这里用的是两个参数的,而不是最开始三个参数的
((AbstractAuthenticationToken)userAuth).setDetails(parameters);
try {
userAuth = this.authenticationManager.authenticate(userAuth);
} catch (AccountStatusException var8) {
throw new InvalidGrantException(var8.getMessage());
} catch (BadCredentialsException var9) {
throw new InvalidGrantException(var9.getMessage());
}
if (userAuth != null && userAuth.isAuthenticated()) {
OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
} else {
throw new InvalidGrantException("Could not authenticate user: " + username);
}
}
来到这里
点进去,这里有点难度,慢慢来
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (this.delegate != null) { //如果debug的话,会发现这里delegate 是null,也就是说会走到 else里面
return this.delegate.authenticate(authentication);
} else {
synchronized(this.delegateMonitor) {
if (this.delegate == null) {
this.delegate = (AuthenticationManager)this.delegateBuilder.getObject();//这里就会用delegateBuilder构造一下delegate
this.delegateBuilder = null; //delegate 构建完成后就把delegateBuilder 置为null,也就是说这个delegateBuilder 是一次性的
}
}
return this.delegate.authenticate(authentication);//因为通过delegateBuilder 构建了delegate ,那么这里就能继续下去
}
}
进到 this.delegate.authenticate(authentication)的authenticate方法里面,代码很多,但是不用急
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass(); //获取类,是我们自己写的JiuBoDouUsernamePasswordAuthenticationToken 这个类
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
Iterator var9 = this.getProviders().iterator();//看起来又是一个遍历
while(var9.hasNext()) {
AuthenticationProvider provider = (AuthenticationProvider)var9.next();
if (provider.supports(toTest)) { //这里就是去看 当前这个 provider是不是支持 JiuBoDouUsernamePasswordAuthenticationToken 不用想了 很定不支持
if (logger.isTraceEnabled()) {
Log var10000 = logger;
String var10002 = provider.getClass().getSimpleName();
++currentPosition;
var10000.trace(LogMessage.format("Authenticating request with %s (%d/%d)", var10002, currentPosition, size));
}
try {
result = provider.authenticate(authentication);
if (result != null) {
this.copyDetails(authentication, result);
break;
}
} catch (InternalAuthenticationServiceException | AccountStatusException var14) {
this.prepareException(var14, authentication);
throw var14;
} catch (AuthenticationException var15) {
lastException = var15;
}
}
}
if (result == null && this.parent != null) {
try {
parentResult = this.parent.authenticate(authentication); //这边还有,去找父类的 provider 是不是有能支持JiuBoDouUsernamePasswordAuthenticationToken 的
result = parentResult;
} catch (ProviderNotFoundException var12) {
} catch (AuthenticationException var13) {
parentException = var13;
lastException = var13;
}
}
if (result != null) {
if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
((CredentialsContainer)result).eraseCredentials();
}
if (parentResult == null) {
this.eventPublisher.publishAuthenticationSuccess(result);
}
return result;
} else {
if (lastException == null) {
lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
}
if (parentException == null) {
this.prepareException((AuthenticationException)lastException, authentication);
}
throw lastException;
}
}
下面我们就去写一个自己的 provider 并放到框架中 同样的,复制一份 改成自己的
但是稍微有些改动,在AbstractUserDetailsAuthenticationProvider中的determineUsername方法是这么写的:
private String determineUsername(Authentication authentication) {
return authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
}
我复制过来后改动了一下:
private String determineUsername(Authentication authentication) {
return authentication.getPrincipal() != null ? authentication.getPrincipal().toString() : "";
}
然后放到框架中,下面是成型的 JiuBoDouWebConfig 代码
@Configuration
public class JiuBoDouWebConfig extends WebSecurityConfigurerAdapter {
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/oauth2/**").permitAll()
.anyRequest().authenticated();
http.cors();/*允许跨域*/
}
@Autowired //下面会有说明
UserDetailsService jiuBoDouUserDetailService;
@Override
protected void configure(AuthenticationManagerBuilder builder) {
//这里就是添加自己的
JiuBoDouDaoAuthenticationProvider provider = new JiuBoDouDaoAuthenticationProvider();
provider.setPasswordEncoder(new BCryptPasswordEncoder());
provider.setUserDetailsService(jiuBoDouUserDetailService);
provider.setUserDetailsPasswordService(new JiuBoDouUserPasswordService());
provider.setHideUserNotFoundExceptions(false);
builder.authenticationProvider(provider);
}
}
重新来一次,断点打在
就能看到自己写的 provider了,进去
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(JiuBoDouUsernamePasswordAuthenticationToken.class, authentication, () -> {
return this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only JiuBoDouUsernamePasswordAuthenticationToken is supported");
});
String username = this.determineUsername(authentication);
boolean cacheWasUsed = true;
UserDetails user = this.userCache.getUserFromCache(username);
if (user == null) {
cacheWasUsed = false;
try {
user = this.retrieveUser(username, (JiuBoDouUsernamePasswordAuthenticationToken)authentication);
} catch (UsernameNotFoundException var6) {
this.logger.debug("Failed to find user '" + username + "'");
if (!this.hideUserNotFoundExceptions) {
throw var6;
}
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
}
try {
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (JiuBoDouUsernamePasswordAuthenticationToken)authentication);
} catch (AuthenticationException var7) {
if (!cacheWasUsed) {
throw var7;
}
cacheWasUsed = false;
user = this.retrieveUser(username, (JiuBoDouUsernamePasswordAuthenticationToken)authentication);
this.preAuthenticationChecks.check(user);
this.additionalAuthenticationChecks(user, (JiuBoDouUsernamePasswordAuthenticationToken)authentication);//这里也需要看一下
}
this.postAuthenticationChecks.check(user);
if (!cacheWasUsed) {
this.userCache.putUserInCache(user);
}
Object principalToReturn = user;
if (this.forcePrincipalAsString) {
principalToReturn = user.getUsername();
}
return this.createSuccessAuthentication(principalToReturn, authentication, user);
}
来到这里
进去
protected final UserDetails retrieveUser(String username, JiuBoDouUsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
this.prepareTimingAttackProtection();
try {
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);//这里进去
if (loadedUser == null) {
throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
} else {
return loadedUser;
}
} catch (UsernameNotFoundException var4) {
this.mitigateAgainstTimingAttack(authentication);
throw var4;
} catch (InternalAuthenticationServiceException var5) {
throw var5;
} catch (Exception var6) {
throw new InternalAuthenticationServiceException(var6.getMessage(), var6);
}
}
发现 this.getUserDetailsService().loadUserByUsername(username) 又出现了一个不知道的东西,UserDetailsService 是什么?发现是一个接口
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
很简单的一个接口,我们自己写一个,实现即可
@Service
public class JiuBoDouUserDetailService implements UserDetailsService {
@Autowired
private UserTableMapper userTableMapper;
@Override//这里其实就是拿着用户传来的 username 去数据库中查这个用户。但是返回值是UserDetails
public UserDetails loadUserByUsername(String userMobile) throws UsernameNotFoundException {
ArrayList<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();
simpleGrantedAuthorities.add(new SimpleGrantedAuthority("admin"));
LambdaQueryWrapper<UserTable> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(UserTable::getUserMobile, userMobile);
UserTable userTable = userTableMapper.selectOne(queryWrapper);
JiuBoDouUserDetails jiuBoDouUserDetails = new JiuBoDouUserDetails(userTable.getUserMobile()
, userTable.getUserPassword(), simpleGrantedAuthorities);
return jiuBoDouUserDetails;
}
}
那么UserDetails又是什么?还是一个接口,我们实现一下
@Data
@AllArgsConstructor
@NoArgsConstructor
public class JiuBoDouUserDetails implements UserDetails {
private String username;/*应该说是 用户的唯一标识 可以是登录时候用的手机号 也可以是id*/
private String password;/*这里的密码是数据库中查出来的 加密之后的密码 不是明文*/
private Collection<? extends GrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
这里就不细讲,代码都很简单。看一下就能明白了,然后就是将 自己写的JiuBoDouUserDetailService 放到框架中,之前已经放过了
继续看下面:
protected void additionalAuthenticationChecks(UserDetails userDetails, JiuBoDouUsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
//这里是密码的匹配,因为从库中查出来用户密码肯定是加密后的,用户输入的密码是明文,这里就是用
String presentedPassword = authentication.getCredentials().toString();
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
继续:比较完成密码之后,认证流程基本就结束了,下面是获取token,来到我们自己写的 JiuBoDouTokenService 里面,找到
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken)
这个方法,打上断点。
因为我们的 JiuBoDouTokenService 是复制于 DefaultTokenServices 的,所以这个方法的代码应该是这样的
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = this.getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (long)validitySeconds * 1000L));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return (OAuth2AccessToken)(this.accessTokenEnhancer != null ? this.accessTokenEnhancer.enhance(token, authentication) : token);
}
而我将其改了改:
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = this.getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (long)validitySeconds * 1000L));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
Map<String, Object> additionalMap = new HashMap<>();
additionalMap.put("principal", authentication.getPrincipal());
additionalMap.put("userAuthentication", authentication.getUserAuthentication());
token.setAdditionalInformation(additionalMap);//也就是这里,将一些信息也放到了token里面,这我们就可以通过token获取到一些用户信息了
return (OAuth2AccessToken)(this.accessTokenEnhancer != null ? this.accessTokenEnhancer.enhance(token, authentication) : token);
}
ok,到此我们的生成token就已经完成了,其实核心是 自己debug一次,很多地方都是能自己改的。
下面说一下刷新token,其实token的作用就是确认这个人是谁,至于有哪些权限,即便token不存,大不了自己查库嘛对吧
@Autowired
private RedisTokenStore redisTokenStore;
@Autowired
private JiuBoDouTokenService jiuBoDouTokenService;
@GetMapping("/do/{token}")
public String doTest(@PathVariable("token") String token) {
OAuth2AccessToken oAuth2AccessToken = redisTokenStore.readAccessToken(token);
if (oAuth2AccessToken == null) {
return "token 不存在,请登录";
}
Map<String, Object> additionalInformation = oAuth2AccessToken.getAdditionalInformation();
JiuBoDouUserDetails userDetails = (JiuBoDouUserDetails) additionalInformation.getOrDefault("principal", null);
String username = userDetails.getUsername();//用户的唯一标识
Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();//用户有的权限
/*token 续命 上述操作能获取到用户的 唯一标识和 权限,但是token续命做不到,这里做一下续命*/
/*如果当前用户的token的剩余时间不足xxxx,那么就换个新的token给他 默认是12小时有效期,也就是43200秒,至于多久换一次token自定*/
if (oAuth2AccessToken.getExpiresIn() < 43100) {
OAuth2AccessToken oAuth2AccessToken1 =
jiuBoDouTokenService.refreshAccessToken(oAuth2AccessToken.getRefreshToken().getValue()
, new TokenRequest(
null,
"jiubodou_client_id",
List.of("all"),
null
));
return JSON.toJSONString(oAuth2AccessToken1);
}
return JSON.toJSONString(oAuth2AccessToken);
}