自定义的方式,使用oauth2生成token+token续命

项目放在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);
    }
  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
OAuth2是一种用于授权的开放标准,用于确保安全和可信任的第三方应用程序可以访问用户的受保护资源。"oauth/token"是OAuth2的端点之一,用于获取访问令牌。在进行OAuth2授权时,客户端应用程序需要提供有效的身份验证凭据以获取访问令牌,从而访问用户的资源。 重写"oauth/token"的主要目的是为了改进OAuth2的安全性和功能。可能会有以下方面的改进: 1. 强化身份验证:重写的"oauth/token"可以实施更强的身份验证措施,例如使用多因素身份验证、生物识别技术等,以确保令牌的颁发仅限于合法的客户端应用程序。 2. 改进令牌的安全性:通过改进令牌的生成算法和存储机制,可以增强令牌的安全性。这可以包括使用更强的加密算法、限制令牌的有效期限和访问范围等。 3. 引入访问审计:通过引入访问审计机制,可以跟踪和监视授权操作的历史记录,以便在出现问题时进行排查和调查。 4. 扩展功能:在重写"oauth/token"的过程中,可以根据特定的业务需求添加自定义功能。例如,可以实现附加的安全验证步骤或添加访问令牌的附加信息。 5. 优化性能:通过对令牌生成和验证过程进行优化,可以提高系统的性能和响应时间。 总结来说,重写"oauth/token"是为了改进OAuth2的安全性、功能和性能。通过引入更强的身份验证措施、改进令牌安全性、添加审计机制和自定义功能,可以提高系统的安全性,并根据业务需求满足更多功能要求。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值