徒手撸springcloud+VUE--Oauth2篇

上一篇写了父工程和eureka、gateway服务的搭建和配置,基本上采用默认的配置就可完成基本的搭建,比较简单。本篇重点写点Oauth2的搭建和踩过的各种坑

一、引入相关jar包

1、oauth2的jar包和自动化配置的jar包

<dependency>
    <groupId>org.springframework.security.oauth</groupId>
    <artifactId>spring-security-oauth2</artifactId>
    <version>2.3.4.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.1.2.RELEASE</version>

2、引入eureka客户端jar包

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

3、本例采用数据库存储token和客户端信息,所以需要引入数据库相关jar包

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.35</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.23</version>
</dependency>
<dependency>
    <groupId>tk.mybatis</groupId>
    <artifactId>mapper-spring-boot-starter</artifactId>
    <version>2.1.5</version>
</dependency>

本例采用的是tk.mybatis,这个jar包基本包含了常用的增删改查功能,而且很方便和Page插件结合,一行代码实现分页查询,感兴趣的小伙伴可以去通用mapper详细了解。
4、本例使用openfeign实现各服务之间互相通信,所以需要引入相应jar包

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<dependency>
    <groupId>com.land</groupId>
    <artifactId>util</artifactId>
    <version>1.0</version>
    <scope>compile</scope>
</dependency>

使用缓存来控制token的刷新,使用util服务中的一些通用类满足各服务之间的用户数据,所以也需要相应的引入其jar包
至此相应jar包都已经加入,后续其他的jar包在需要使用时加入。

二、配置WebSecurityConfigurerAdapter

1、新建WebSecurityConfig类继承WebSecurityConfigurerAdapter,代码如下

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    
    @Autowired
    private UserServiceDetail userServiceDetail;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
    
    
        //禁用跨站伪造
        http
                .authorizeRequests().anyRequest().authenticated();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    
    
        auth
                .userDetailsService(userServiceDetail)
                .passwordEncoder(new BCryptPasswordEncoder());
    }
}

配置所有的访问都必须经过验证,并设置了获取用户的service和密码验证方式。
其中UserServiceDetail为验证用户的方法,需要继承UserDetailsService,代码如下

@Service
public class UserServiceDetail implements UserDetailsService {
    
    
    @Autowired
    private SysFeignService sysFeignService;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
    
    
        UserDto userDto = sysFeignService.loadUserByUsername(userName);
        if(null != userDto){
    
    
            User user = new User(userDto);
            return user;
        }else {
    
    
            throw new UsernameNotFoundException("账号不存在");
        }
    }
}

此处采用openfeign的方式访问远程sys服务,获取用户的相关信息,代码如下

@FeignClient(name = "sys",configuration = FeignConfig.class)
public interface SysFeignService {
    
    
    @GetMapping(value = "/common/loadUserByUsername")
    UserDto loadUserByUsername(@RequestParam("userName") String userName);

    @GetMapping(value = "/common/findByName")
    UserDto findByName(@RequestParam("userName") String userName);

}

两个获取方式,其中一个是获取管理员的,一个是获取普通用户的,稍后再详细说明
使用@FeignClient注解启用远程访问,其中name参数指远程服务的名称,configuration指配置文件,我的配置文件采用的是默认的,代码如下

@Configuration
public class FeignConfig {
    
    
    @Bean
    public Retryer feignRetryer(){
    
    
        return new Retryer.Default();
    }
}

三、配置AuthorizationServerConfigurerAdapter

新建Oauth2Config继承AuthorizationServerConfigurerAdapter
1、创建tokenStore方法,并注册为bean

@Bean
public TokenStore tokenStore(){
    
    
    //使用数据库存储token信息
    return new JdbcTokenStore(dataSource);
//  return new JwtTokenStore(jwtAccessTokenConverter());
}

tokenStore有4种方式存储,分别是内存、数据库、jwt和Redis,具体可打开TokenStore源码,点击其实现类查看,如下图
在这里插入图片描述
本例采用数据的存储方式,主要是为了后期能在管理端直接控制用户token
2、配置客户端存储方式,代码如下

@Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    
    
        //使用数据库存储客户端信息
        clients.jdbc(dataSource);
    }

客户端的存储方式有两种,数据库和内存存储,可见下方源码

public class ClientDetailsServiceConfigurer extends SecurityConfigurerAdapter<ClientDetailsService, ClientDetailsServiceBuilder<?>> {
    
    
    public ClientDetailsServiceConfigurer(ClientDetailsServiceBuilder<?> builder) {
    
    
        this.setBuilder(builder);
    }

    public ClientDetailsServiceBuilder<?> withClientDetails(ClientDetailsService clientDetailsService) throws Exception {
    
    
        this.setBuilder(((ClientDetailsServiceBuilder)this.getBuilder()).clients(clientDetailsService));
        return (ClientDetailsServiceBuilder)this.and();
    }

    public InMemoryClientDetailsServiceBuilder inMemory() throws Exception {
    
    
        InMemoryClientDetailsServiceBuilder next = ((ClientDetailsServiceBuilder)this.getBuilder()).inMemory();
        this.setBuilder(next);
        return next;
    }

    public JdbcClientDetailsServiceBuilder jdbc(DataSource dataSource) throws Exception {
    
    
        JdbcClientDetailsServiceBuilder next = ((ClientDetailsServiceBuilder)this.getBuilder()).jdbc().dataSource(dataSource);
        this.setBuilder(next);
        return next;
    }

    public void init(ClientDetailsServiceBuilder<?> builder) throws Exception {
    
    
    }

    public void configure(ClientDetailsServiceBuilder<?> builder) throws Exception {
    
    
    }
}

本例采用的同样是数据库方式,原因也是为了能动态的控制各客户端的访问权限,例如账号密码、资源ID、scope和token有效期等。但需要配合token管理,因为修改客户端信息后不会马上生效,已获取token的客户端已经将相关信息存放到token中,在token失效之前不会再访问数据库进行验证。因此如果想修改立马生效,可以将已生成的token全部清除。
3、配置获取token的策略

@Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    
    
    security.tokenKeyAccess("permitAll()")
            .checkTokenAccess("isAuthenticated()")
            .allowFormAuthenticationForClients()
            .passwordEncoder(new BCryptPasswordEncoder());
}

4、配置AuthorizationServerEndpointsConfigurer

@Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    
    
        endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager);
        endpoints.userDetailsService(userServiceDetail);
    }

四、扩展自定义登录方式

1、在配置AuthorizationServerEndpointsConfigurer中设置tokenGranter为自定义tokenGranter

endpoints.tokenGranter(tokenGranter(endpoints));

2、自定义tokenGranter方法

private TokenGranter tokenGranter(AuthorizationServerEndpointsConfigurer endpoints) {
    
    
        TokenGranter tokenGranter = new TokenGranter() {
    
    
            private CompositeTokenGranter delegate;

            @Override
            public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
    
    
                if (delegate == null) {
    
    
                    delegate = new CompositeTokenGranter(getDefaultTokenGranters(endpoints));
                }
                return delegate.grant(grantType, tokenRequest);
            }
        };
        return tokenGranter;
    }

通过getDefaultTokenGranters方法重写默认TokenGranter
3、自定义getDefaultTokenGranters方法

private List<TokenGranter> getDefaultTokenGranters(AuthorizationServerEndpointsConfigurer endpoints) {
    
    
        AuthorizationServerTokenServices tokenService = endpoints.getTokenServices();
        OAuth2RequestFactory requestFactory = endpoints.getOAuth2RequestFactory();
        AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();

        List<TokenGranter> tokenGranters = new ArrayList<>();
        tokenGranters.add(new ClientCredentialsTokenGranter(tokenService, clientDetailsService, requestFactory));
        tokenGranters.add(new RefreshTokenGranter(tokenService, clientDetailsService, requestFactory));
        tokenGranters.add(new AuthorizationCodeTokenGranter(tokenService, authorizationCodeServices, clientDetailsService, requestFactory));
        tokenGranters.add(new ImplicitTokenGranter(tokenService, clientDetailsService, requestFactory));
        tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenService, clientDetailsService, requestFactory));

        //短信验证码授权
        tokenGranters.add(new SmsCodeTokenGranter(tokenService, clientDetailsService, requestFactory,sysFeignService));
        //管理用户账号密码登录
        tokenGranters.add(new PasswordTokenGranter(authenticationManager,tokenService, clientDetailsService, requestFactory,sysFeignService));
        //普通用户账号密码登录
        tokenGranters.add(new UserPasswordTokenGranter(authenticationManager,tokenService, clientDetailsService, requestFactory,sysFeignService));
        return tokenGranters;
    }

首先在tokenGranters 列表中加入默认的tokenGranter,包括客户端认证、token刷新等,在下方加入自定义的登录方法。
4、自定义登录方式
以管理员账号登录为例,继承AbstractTokenGranter ,代码如下

public class PasswordTokenGranter extends AbstractTokenGranter {
    
    
    private static final String GRANT_TYPE = "sys_password";
    private final AuthenticationManager authenticationManager;
    private SysFeignService sysFeignService;


    protected PasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, SysFeignService sysFeignService) {
    
    
        super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
        this.sysFeignService = sysFeignService;
        this.authenticationManager = authenticationManager;
    }

    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
    
    
        Map<String, String> parameters = new LinkedHashMap(tokenRequest.getRequestParameters());
        String username = parameters.get("username");
        String password = parameters.get("password");

        UserDto userDto = sysFeignService.loadUserByUsername(username);
        if(userDto == null) {
    
    
            throw new InvalidGrantException("用户不存在");
        }
        if(!new BCryptPasswordEncoder().matches(password,userDto.getPassword())){
    
    
            throw new InvalidGrantException("密码不正确");
        }
        User user = new User(userDto);
        Authentication userAuth = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
        ((AbstractAuthenticationToken)userAuth).setDetails(userDto);

        OAuth2Request storedOAuth2Request = this.getRequestFactory().createOAuth2Request(client, tokenRequest);
        return new OAuth2Authentication(storedOAuth2Request, userAuth);
    }

}

自定义GRANT_TYPE 为sys_password,即在登录时GRANT_TYPE 参数为sys_password时会自动进入到此方法进行登录处理
重写getOAuth2Authentication方法,进行登录逻辑处理
这里有三个坑,一个是密码验证方式,一个是bean注入方式,一个是客户端只能获取用户名,其他信息无法获取

  • 密码验证方式

密码验证方式和常规的MD5验证方式不同,只能使用BCryptPasswordEncoder内置的matches方法进行验证,因为BCryptPasswordEncoder是不对称加密,同样的明文每次加密后的密文都是不同的。这个问题是对BCryptPasswordEncoder的不了解,坑了我好长时间,最后发现登陆传的密码和数据库密码不一致,然后自己再次加密的时候,发现还是跟数据库不同,才找到问题所在,说出来有点丢人。。。

  • bean注入方式
    必须在Oauth2Config类中注入,然后在自定义方法中将bean传到这里,然后初始化的时候赋值,否则bean会一直为null,具体原因未知。在这里被坑了整整一天。。。。
    1、在Oauth2Config中注入sysFeignService
@Autowired
private SysFeignService sysFeignService;

2、在自定义方法中将bean传过去

tokenGranters.add(new PasswordTokenGranter(authenticationManager,tokenService, clientDetailsService, requestFactory,sysFeignService));

2、在PasswordTokenGranter初始化方法中赋值

private final AuthenticationManager authenticationManager;
    private SysFeignService sysFeignService;


    protected PasswordTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, SysFeignService sysFeignService) {
    
    
    super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
    this.sysFeignService = sysFeignService;
    this.authenticationManager = authenticationManager;
}

3、扩展客户端获取用户信息

User user = new User(userDto);
Authentication userAuth = new UsernamePasswordAuthenticationToken(user,null,user.getAuthorities());
((AbstractAuthenticationToken)userAuth).setDetails(userDto);

其中User继承了security的UserDetails,通过user进行验证方便后期扩展,不过这是我的强迫症,实际上通过userDto也可以。然后通过setDetails方法将用户信息放到token中,在资源服务中就可以通过下面的方法获取userDto数据

public static UserDto getUser(){
    
    
    try {
    
    
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        JSONObject authJson = JSONObject.parseObject(JSONObject.toJSONString(authentication));
        JSONObject userAuthJson = authJson.getJSONObject("userAuthentication");
        JSONObject detailsJson = userAuthJson.getJSONObject("details");
        JSONObject principanJson = detailsJson.getJSONObject("principal");
        UserDto userDto = new UserDto();
        userDto.setUsername(principanJson.getString("username"));
        userDto.setUserType(principanJson.getString("userType"));
        userDto.setEnabled(principanJson.getBoolean("enabled"));
        userDto.setAuthList(principanJson.getJSONArray("authorities").toJavaList(String.class));
        userDto.setBirthday(principanJson.getDate("birthday"));
        userDto.setSex(principanJson.getString("sex"));
        userDto.setMail(principanJson.getString("mail"));
        userDto.setRealName(principanJson.getString("realName"));
        userDto.setNickName(principanJson.getString("nickName"));
        userDto.setId(principanJson.getInteger("id"));
        userDto.setPhone(principanJson.getString("phone"));
        return userDto;
    }catch (Exception e){
    
    
        return null;
    }
}

当然也可以在controller中直接将Principal 、 OauthApplication 、 Authentication 对象传进去直接获取,我这里因为使用自定义注解实现日志服务,这几个对象解析是会报错,所以就写了一个工具类来获取,后文在介绍sys服务时会说。

五、token自动刷新

客户端信息已经配置了token的有效期,本例采用数据存储,数据库必须通过官方的语句进行生成,数据语句可以在官网和网上获取
数据库结构如下图
在这里插入图片描述
本例只使用oauth_client_details,oauth_access_token两张表

oauth_access_token表如下
oauth_access_token
oauth_client_details表如下
在这里插入图片描述
其中access_token_validity即是token有效期
但是token到期后还得引导用户重新进行登录,体验非常不好,如果能像session一样多长时间不访问才会过期就好了,基于这个想法,进行了各种实验,最终找到一种方法,具体做法如下
1、自定义OauthTokenInterceptor拦截器继承HandlerInterceptorAdapter

@Component
public class OauthTokenInterceptor extends HandlerInterceptorAdapter {
    
    
    @Autowired
    private TokenStore tokenStore;

//    @Autowired
//    private RedisTemplate<String,String> redisTemplate;

    private final Cache cache = Cache.newHardMemoryCache(0,3*60);

    private final long hour = 2;

    //拦截请求,如果存在token,自动重置token有效期
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    
    
        String authorization = request.getHeader("Authorization");
        if(StringUtils.isNotBlank(authorization)){
    
    
            authorization = authorization.substring(authorization.indexOf("Bearer")+6).trim();
            DefaultOAuth2AccessToken defaultOAuth2AccessToken = (DefaultOAuth2AccessToken) tokenStore.readAccessToken(authorization);
            if(!Objects.isNull(defaultOAuth2AccessToken) && defaultOAuth2AccessToken.isExpired()){
    
    
                //token已过期,返回
                return super.preHandle(request,response,handler);
            }
            if(null == cache.get(authorization)){
    
    
                //将token放到缓存中,有效期设置为3分钟,即3分钟之刷新一次
                cache.put(authorization,new Date());
                System.out.println("============================更新token:"+authorization+"("+new Date()+")===============================");
                //获取到期时间
                Date expiration = defaultOAuth2AccessToken.getExpiration();
                //给到期时间重置为两个小时
                expiration.setTime(new Date().getTime() + hour*60*60*1000L);
                defaultOAuth2AccessToken.setExpiration(expiration);
                //重新设置token
                OAuth2Authentication oauthApplication = tokenStore.readAuthentication(defaultOAuth2AccessToken);
                tokenStore.storeAccessToken(defaultOAuth2AccessToken,oauthApplication);
            }
        }
        return super.preHandle(request,response,handler);
    }
}

重写preHandle方法
首先判断header中是否有token,header中的token是放在Authorization参数中,这个前后端要统一,token前面会加上token类型Bearer,这里也要前后端统一,然后进行截取,获取token
如果没有token直接放行
如果有token,判断token是否过期,如果过期直接放行
如果token未过期,解析token,并重新设置token有效期,本例每次都将token有效期重置为两个小时,实际情况可以根据业务进行调整
这里有个坑,就是重新设置token的时候会报错,原因有以下几个

  • 重新设置token,会先删除原token,然后在插入新的token,源码如下
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
    
    
        String refreshToken = null;
        if (token.getRefreshToken() != null) {
    
    
            refreshToken = token.getRefreshToken().getValue();
        }

        if (this.readAccessToken(token.getValue()) != null) {
    
    
            this.removeAccessToken(token.getValue());
        }

        this.jdbcTemplate.update(this.insertAccessTokenSql, new Object[]{
    
    this.extractTokenKey(token.getValue()), new SqlLobValue(this.serializeAccessToken(token)), this.authenticationKeyGenerator.extractKey(authentication), authentication.isClientOnly() ? null : authentication.getName(), authentication.getOAuth2Request().getClientId(), new SqlLobValue(this.serializeAuthentication(authentication)), this.extractTokenKey(refreshToken)}, new int[]{
    
    12, 2004, 12, 12, 12, 2004, 12});
    }

insertAccessTokenSql 语句

private String insertAccessTokenSql = "insert into oauth_access_token (token_id, token, authentication_id, user_name, client_id, authentication, refresh_token) values (?, ?, ?, ?, ?, ?, ?)";

removeAccessToken方法

public void removeAccessToken(String tokenValue) {
    
    
        this.jdbcTemplate.update(this.deleteAccessTokenSql, new Object[]{
    
    this.extractTokenKey(tokenValue)});
    }

deleteAccessTokenSql语句

private String deleteAccessTokenSql = "delete from oauth_access_token where token_id = ?";

这里删除的时候是直接将token删除了,但是如果同时使用refreshToken 的话会造成oauth_refresh_token表外键找不到
oauth_refresh_token表结构如下
在这里插入图片描述
所以需要放弃使用refreshToken (自认为,不知道对不对,结果就是我不使用refreshToken 后这个问题解决了。。。)

  • 频繁刷新脏数据问题
    上面的代码中,各位小伙伴可以看到里面有一个判断缓存中是否有token的步骤,主要就是解决这个问题的。原因是我登录的时候需要获取token,获取当前登录用户,获取菜单、权限等等,所以会在登录的一瞬间多次访问,导致解析的时候token已经不存在了(原来的已经删除,新的还未生成,导致token解析失败)
    为了解决这个问题,刚开始是采用Redis存储的方式,每次刷新token之后将token存放到Redis中,每次更新前先判断Redis中是否已经存在,如果存在就不再更新了。首先考虑Redis的原因是可以通过Redis客户端控制刷新,而且Redis支持自定义有效期,这样才能控制token的刷新频率。
  • Redis读取太慢
    使用Redis之后上述的问题得到了改善,但是偶尔还是会出问题,而且并发高峰期出问题的频率比较高,问题个人怀疑是Redis读取速度的问题(我的Redis是放在外网服务器上的,个人在本地调试,存在一定的网络时间),所以将Redis更换为cache缓存,至此个人测试阶段未再出现这个问题。
    不过这个解决办法并不是最好的,因为如果要实现oauth服务高可用如果设置全局,这个问题留待以后有时间再考虑。

2、将自定义拦截器加入到SpringMVC拦截器中
新建WebMvcConfig类继承WebMvcConfigurer

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    
    
    @Autowired
    private OauthTokenInterceptor oauthTokenInterceptor;

    //添加自定义拦截器
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
    
    
        registry.addInterceptor(oauthTokenInterceptor).addPathPatterns("/**");
    }
}

这里踩了个坑,自定义的OauthTokenInterceptor 拦截器需要用注入bean的方式使用,不能用new的方式,否则OauthTokenInterceptor 类会报空指针。网上看的一些方法直接new就可以用,我的不可以,具体原因未深入探究。

六、配置文件

所有的配置如下:

server:
  port: 8768

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka

spring:
  application:
    name: oauth
  datasource:
    driver-class-name: com.mysql.jdbc.Driver
    url: jdbc:mysql://localhost:3306/longmao-oauth?serverTimezone=UTC&useSSL=false&allowPublicKeyRetrieval=true&useUnicode=true&characterEncoding=UTF-8
    username: root
    password: root
    # 连接池配置
    druid:
      # 初始化大小,最小,最大
      initial-size: 5
      min-idle: 5
      max-active: 20
      # 配置获取连接等待超时的时间
      max-wait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位毫秒
      time-between-eviction-runs-millis: 60000
      # 配置一个连接在池中最小生存时间
      min-evictable-idle-time-millis: 300000
      validation-query: SELECT 1 FROM sys_user
      test-while-idle: true
      test-on-borrow: false
      test-on-return: false
      # 打开 PSCache,并且指定每个连接上 PSCache 的大小
      pool-prepared-statements: true
      max-pool-prepared-statement-per-connection-size: 20
      # 配置监控统计拦截的 Filter,去掉后监控界面 SQL 无法统计,wall 用于防火墙
      filters: stat,wall
      # 通过 connection-properties 属性打开 mergeSql 功能;慢 SQL 记录
      connection-properties: druid.stat.mergeSql\=true;druid.stat.slowSqlMillis\=5000
      # 配置 DruidStatFilter
      web-stat-filter:
        enabled: true
        url-pattern: /*
        exclusions: .js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*
      # 配置 DruidStatViewServlet
      stat-view-servlet:
        enabled: true
        url-pattern: /druid/*
        # IP 白名单,没有配置或者为空,则允许所有访问
        #        allow: 127.0.0.1
        # IP 黑名单,若白名单也存在,则优先使用
        #        deny: 192.168.31.253
        # 禁用 HTML 中 Reset All 按钮
        reset-enable: false
        # 登录用户名/密码
        login-username: root
        login-password: 123
  redis:
    database: 0
    host: localhost
    password: test123
    port: 6379
    timeout: 1000

七、总结

oauth2对我来说还是个新东西,刚开始写的时候下手太急了,导致后续进行了多次更改。对于新技术最好还是通过几个步骤来学习会更好些

  1. 通过网上查询相关资料,了解技术背景、主要解决方向和实现逻辑
  2. 查询相关案例,读代码
  3. 查看源码,了解实现的具体方法
  4. 下手尝试,深入理解

以上代码都可以直接拿来用,已经经过个人测试,基本解决了我现在的所有需求,有问题可以留言讨论。

猜你喜欢

转载自blog.csdn.net/ZhangYongHuiab/article/details/108629640