0%

SpringBoot-OAuth2-JWT-AuthServer

摘要

依赖

1
2
3
4
5
6
7
8
9
10
11
implementation 'org.springframework.boot:spring-boot-starter-web'

//SpringBoot官方没有提供对认证服务的支持,需要自己搭建,
//我们可以使用SpringSecurity提供的spring-security-oauth2-autoconfigure进行搭建,
//不过SpringSecurity OAuth2项目目前已被弃用,SpringSecurity团队已决定不再为授权服务器提供支持,
//所以,在这个版本里你会看到很多弃用的类,如果不想看到类名称上出现烦人的横线,可以使用2.2.10这个版本,测试过程也没发现什么问题的。
implementation 'org.springframework.security.oauth.boot:spring-security-oauth2-autoconfigure:2.3.4.RELEASE'

//本项目中使用了lombok简少代码量,非必须依赖
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'

代码说明

SecurityConfig

  • 用于配置可以登录系统的用户及其权限,也就是认证服务器的用户

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    140
    141
    142
    143
    144
    145
    146
    package com.example.oauth2authserverdemo.config;

    import com.example.oauth2authserverdemo.security.CustomSecurityProperties;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.BeanIds;
    import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.core.authority.SimpleGrantedAuthority;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken;
    import org.springframework.security.provisioning.InMemoryUserDetailsManager;
    import org.springframework.web.cors.CorsConfiguration;
    import org.springframework.web.cors.CorsConfigurationSource;
    import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

    import java.util.Collection;
    import java.util.stream.Collectors;

    /**
    * Spring-Security配置类,继承WebSecurityConfigurerAdapter
    *
    */
    @Configuration
    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomSecurityProperties customSecurityProperties;

    /**
    * 引入密码加密类
    *
    * @return
    */
    @Bean
    public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
    }


    /**
    * 为了测试方便,这里使用内存用户模型,真实系统请使用基于RBAC的权限模型
    * 用户策略设置,这里使用内存用户策略,自定义策略需要实现UserDetailsService接口
    */
    @Override
    @Bean
    public UserDetailsService userDetailsService() {
    InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
    User.UserBuilder builder = User.builder().passwordEncoder(passwordEncoder()::encode);
    inMemoryUserDetailsManager.createUser(builder.username("admin").password("123456").roles("admin").build());
    inMemoryUserDetailsManager.createUser(builder.username("guest").password("123456").roles("guest").build());
    inMemoryUserDetailsManager.createUser(builder.username("user").password("123456").roles("user").build());
    return inMemoryUserDetailsManager;
    }


    /**
    * 指定不拦截的路径规则
    */
    @Override
    public void configure(WebSecurity web) throws Exception {
    //设置不需要拦截的路径,也就是不需要认证的路径
    web.ignoring().antMatchers(customSecurityProperties.getIgnoring());
    }

    /**
    * 配置URL访问授权,必须配置authorizeRequests(),否则启动报错,说是没有启用security技术。
    * 注意:在这里的身份进行认证与授权没有涉及到OAuth的技术:当访问要授权的URL时,请求会被DelegatingFilterProxy拦截,
    * 如果还没有授权,请求就会被重定向到登录界面。在登录成功(身份认证并授权)后,请求被重定向至之前访问的URL。
    *
    * @param http
    * @throws Exception
    */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.formLogin()
    .permitAll(); //登记界面,默认是permitAll

    http.authorizeRequests()
    .antMatchers(customSecurityProperties.getPermitAll()).permitAll() //不用身份认证可以访问
    .anyRequest().authenticated(); //其它的请求要求必须有身份认证

    http.csrf().disable();

    //开启跨域
    http.cors();

    // session管理
    http.sessionManagement()
    .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);


    }


    /**
    * 支持 password 模式(配置)
    *
    * @return
    * @throws Exception
    */
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
    }


    /**
    * 跨域配置类
    */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration corsConfiguration = new CorsConfiguration().applyPermitDefaultValues();
    //开放哪些ip、端口、域名的访问权限,星号表示开放所有域
    corsConfiguration.addAllowedOrigin("*");
    //corsConfiguration.setAllowedOrigins(Arrays.asList("http://localhost:8080","http://localhost:8081"));
    //开放哪些Http方法,允许跨域访问
    corsConfiguration.addAllowedMethod("*");
    //corsConfiguration.setAllowedMethods(Arrays.asList("GET","POST", "PUT", "DELETE"));
    //允许HTTP请求中的携带哪些Header信息
    corsConfiguration.addAllowedHeader("*");
    //是否允许发送Cookie信息
    corsConfiguration.setAllowCredentials(true);

    //添加映射路径,“/**”表示对所有的路径实行全局跨域访问权限的设置
    UrlBasedCorsConfigurationSource configSource = new UrlBasedCorsConfigurationSource();
    configSource.registerCorsConfiguration("/**", corsConfiguration);

    return configSource;
    }


    }

    CustomSecurityProperties

  • 自定义属性配置类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @Data
    @ConfigurationProperties(prefix = "security")
    @Component
    public class CustomSecurityProperties {
    /**
    * 不需要验证的路径数组
    */
    private String[] permitAll;
    /**
    * 不需要拦截的路径数组
    */
    private String[] ignoring;
    }
  • 配置文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #springsecurity 自定义属性
    security:
    #不需要验证的路径
    permitAll:
    - /redirect/**
    ignoring:
    - /webjars/**
    - /**/*.js/
    - /**/*.css
    - /static/**

AuthServerConfig

  • 认证服务器的token关联,本例基于jwt
  • 注意这里定义的是抽象类,还没有声明客户端的配置,具体参加下文说明
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    package com.example.oauth2authserverdemo.config;

    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.HttpMethod;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
    import org.springframework.security.oauth2.provider.token.TokenEnhancer;
    import org.springframework.security.oauth2.provider.token.TokenEnhancerChain;
    import org.springframework.security.oauth2.provider.token.TokenStore;
    import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;

    import java.util.ArrayList;
    import java.util.List;

    public abstract class AuthServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    protected UserDetailsService userDetailsService;

    @Autowired
    protected AuthenticationManager authenticationManager;

    @Autowired
    protected TokenStore jwtTokenStore;

    @Autowired
    protected JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired
    protected TokenEnhancer jwtTokenEnhancer;

    @Autowired
    protected PasswordEncoder passwordEncoder;


    /**
    * 1.增加jwt 增强模式
    * 2.调用userDetailsService实现UserDetailsService接口,对客户端信息进行认证与授权
    *
    * @param endpoints
    * @throws Exception
    */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    // jwt 增强模式
    // 对令牌的增强操作就在enhance方法中
    // 面在配置类中,将TokenEnhancer和JwtAccessConverter加到一个enhancerChain中
    //
    // 通俗点讲它做了两件事:
    // 给JWT令牌中设置附加信息和jti:jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击
    // 判断请求中是否有refreshToken,如果有,就重新设置refreshToken并加入附加信息
    TokenEnhancerChain enhancerChain = new TokenEnhancerChain();
    List<TokenEnhancer> enhancerList = new ArrayList<TokenEnhancer>();
    enhancerList.add(jwtTokenEnhancer);
    enhancerList.add(jwtAccessTokenConverter);
    enhancerChain.setTokenEnhancers(enhancerList); //将自定义Enhancer加入EnhancerChain的delegates数组中

    endpoints
    //设置tokenStore
    .tokenStore(jwtTokenStore)
    //支持 refresh_token 模式
    .userDetailsService(userDetailsService)
    //支持 password 模式
    .authenticationManager(authenticationManager)
    //token扩展属性
    .tokenEnhancer(enhancerChain)
    //设置token转换器
    .accessTokenConverter(jwtAccessTokenConverter)
    //设置每次通过refresh_token获取新的access_token时,是否重新生成一个新的refresh_token。
    //默认true,不重新生成
    //.reuseRefreshTokens(true)
    //设置允许处理的请求类型
    .allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST);
    }


    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    security
    //获取公钥的请求全部允许
    .tokenKeyAccess("permitAll()")
    //验证token有效性的请求需要先登录认证
    .checkTokenAccess("isAuthenticated()")
    //允许客户端form表单认证,就是可以将client_id和client_secret放到form中提交,否则必须使用Basic认证
    .allowFormAuthenticationForClients();
    }


    }

JwtTokenConfig

  • 本例基于JWTToken
  • 支持两种密钥策略,对称密钥和基于jks证书的非对称密钥
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    package com.example.oauth2authserverdemo.config;

    import com.example.oauth2authserverdemo.security.jwt.JWTokenEnhancer;
    import com.example.oauth2authserverdemo.security.jwt.JwtTokenProperties;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.oauth2.provider.token.TokenEnhancer;
    import org.springframework.security.oauth2.provider.token.TokenStore;
    import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
    import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
    import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

    /**
    * JwtTokenConfig配置类
    * 使用TokenStore将引入JwtTokenStore
    *
    * 注:Spring-Sceurity使用TokenEnhancer和JwtAccessConverter增强jwt令牌
    */
    @Configuration
    public class JwtTokenConfig {

    @Autowired
    private JwtTokenProperties jwtTokenProperties;

    @Bean
    public TokenStore jwtTokenStore() {
    return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
    * JwtAccessTokenConverter:TokenEnhancer的子类,帮助程序在JWT编码的令牌值和OAuth身份验证信息之间进行转换(在两个方向上),同时充当TokenEnhancer授予令牌的时间。
    * 自定义的JwtAccessTokenConverter:把自己设置的jwt签名加入accessTokenConverter中(这里设置'demo',项目可将此在配置文件设置)
    * @return
    */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
    JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();

    String type = jwtTokenProperties.getType();

    switch (type){
    case "secret":
    //设置加密token的密码
    accessTokenConverter.setSigningKey(jwtTokenProperties.getSecret());
    //实际上资源服务器只需要设置验证token的密码
    accessTokenConverter.setVerifierKey(jwtTokenProperties.getSecret());
    break;
    case "jks":
    //非对称加密,jks证书
    KeyStoreKeyFactory keyStoreKeyFactory =
    new KeyStoreKeyFactory(jwtTokenProperties.getJksKeyFileResource(), jwtTokenProperties.getJksStorePassword().toCharArray());
    accessTokenConverter.setKeyPair(keyStoreKeyFactory.getKeyPair(jwtTokenProperties.getJksKeyAlias(),jwtTokenProperties.getJksKeyPassword().toCharArray()));
    break;
    default:
    throw new RuntimeException("请正确配置密钥类型:secret,jks");
    }

    return accessTokenConverter;
    }

    /**
    * 引入自定义JWTokenEnhancer:
    * 自定义JWTokenEnhancer实现TokenEnhancer并重写enhance方法,将附加信息加入oAuth2AccessToken中
    * @return
    */
    @Bean
    public TokenEnhancer jwtTokenEnhancer(){
    return new JWTokenEnhancer();
    }
    }

JWTokenEnhancer

  • jwt自定义属性
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    package com.example.oauth2authserverdemo.security.jwt;

    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
    import org.springframework.security.oauth2.common.OAuth2AccessToken;
    import org.springframework.security.oauth2.provider.OAuth2Authentication;
    import org.springframework.security.oauth2.provider.token.TokenEnhancer;

    import java.util.HashMap;
    import java.util.Map;

    /**
    * TokenEnhancer:在AuthorizationServerTokenServices 实现存储访问令牌之前增强访问令牌的策略。
    * 自定义TokenEnhancer的代码:把附加信息加入oAuth2AccessToken中
    *
    */
    public class JWTokenEnhancer implements TokenEnhancer {

    /**
    * 重写enhance方法,将附加信息加入oAuth2AccessToken中
    * 这里只是提供附加信息的方式,没需要可以不配置这个类
    * @param oAuth2AccessToken
    * @param oAuth2Authentication
    * @return
    */
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
    Map<String, Object> map = new HashMap<String, Object>();
    User user = (User) oAuth2Authentication.getUserAuthentication().getPrincipal();
    map.put("_username", user.getUsername());
    map.put("_authorities", user.getAuthorities());
    map.put("_jwt-ext", "JWT 扩展信息");
    ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(map);
    return oAuth2AccessToken;
    }
    }

JwtTokenProperties

  • 自定义的jwt相关属性类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    package com.example.oauth2authserverdemo.security.jwt;

    import lombok.Data;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.core.io.ClassPathResource;
    import org.springframework.core.io.PathResource;
    import org.springframework.core.io.Resource;
    import org.springframework.stereotype.Component;

    import java.nio.file.Paths;

    /**
    * Jwt工具类
    */
    @Data
    @ConfigurationProperties(prefix = "jwt")
    @Component
    public class JwtTokenProperties {

    /**
    * 密钥类型:secret,jks
    */
    private String type;

    /**
    * 密钥
    */
    private String secret;
    /**
    * accessToken过期时间,单位秒
    */
    private Integer accessTokenValiditySeconds;

    /**
    * refreshToken过期时间,单位秒
    */
    private Integer refreshTokenValiditySeconds;

    /**
    * jks证书文件路径
    */
    private String jksKeyFile;

    /**
    * jks证书密钥库密码
    */
    private String jksStorePassword;
    /**
    * jks证书密码
    */
    private String jksKeyPassword;
    /**
    * jks证书别名
    */
    private String jksKeyAlias;


    /**
    * 获取jks证书Resource
    */
    public Resource getJksKeyFileResource(){
    Resource resource;
    if (jksKeyFile.startsWith("classpath:")) {
    resource = new ClassPathResource(jksKeyFile.replace("classpath:", ""));
    } else {
    resource = new PathResource(Paths.get(jksKeyFile));
    }
    return resource;
    }

    }
  • 配置文件:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #自定义jwt属性信息
    jwt:
    type: jks # 密钥类型:secret,jks
    # SECRET 是签名密钥,只生成一次即可,生成方法:
    # Key key = Keys.secretKeyFor(SignatureAlgorithm.HS512);
    # String secretString = Encoders.BASE64.encode(key.getEncoded()); # 使用 BASE64 编码
    secret: Ayl7bn+aFwxlakekKCJiqUYguKS80bEVb7OZtd2qfZjdCbAwKxDmM6PWezGy5JIkiJfemtHNPc7Av1l+OWQSqQ== # 秘钥
    accessTokenValiditySeconds: 43200 # access_token过期时间 (秒) ,默认值12小时
    refreshTokenValiditySeconds: 2592000 # refresh_token过期时间 (秒) ,默认值30天
    jksKeyFile: classpath:oauth2_key.jks
    jksStorePassword: 123456
    jksKeyAlias: oauth2
    jksKeyPassword: 123456

客户端配置

  • Oauth2支持两种客户端配置方式,基于内存和基于数据库

    AuthServerConfigByMemory

  • 基于内存
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    package com.example.oauth2authserverdemo.config;

    import com.example.oauth2authserverdemo.security.jwt.JwtTokenProperties;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

    @Configuration
    @EnableAuthorizationServer
    @ConditionalOnProperty(prefix = "oauth2.clients.config",name = "jdbc",havingValue = "false",matchIfMissing = true)
    @Slf4j
    public class AuthServerConfigByMemory extends AuthServerConfig{

    @Autowired
    private JwtTokenProperties jwtTokenProperties;
    /**
    * 配置OAuth2的客户端信息:clientId、client_secret、authorization_type、redirect_url等。
    *
    * @param clients
    * @throws Exception
    */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.inMemory()
    .withClient("postman")
    .secret(passwordEncoder.encode("postman"))
    .scopes("any","all") //指定用户的访问范围
    //.autoApprove(true) //设置true跳过用户确认授权操作页面直接同意,默认false,必须设置scopes
    .autoApprove("any") //指定客户端访问哪些范围时,跳过用户确认授权操作页面直接同意,必须设置scopes,请求参数必须加上&scope=any
    .authorizedGrantTypes("password", "authorization_code", "refresh_token","implicit","client_credentials")
    .redirectUris("http://localhost:8080/redirect")
    .accessTokenValiditySeconds(jwtTokenProperties.getAccessTokenValiditySeconds()) //默认12小时
    .refreshTokenValiditySeconds(jwtTokenProperties.getRefreshTokenValiditySeconds()) //默认30天
    .and()
    .withClient("demo-client")
    .secret(passwordEncoder.encode("demo-client"))
    .scopes("any")
    .authorizedGrantTypes("password", "authorization_code", "refresh_token","implicit")
    .redirectUris("http://localhost:8080/redirect");
    log.info("OAuth2的client信息基于内存");
    }
    }

AuthServerConfigByJDBC

  • 基于数据库

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    package com.example.oauth2authserverdemo.config;

    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

    import javax.sql.DataSource;

    @Slf4j
    @Configuration
    @EnableAuthorizationServer
    @ConditionalOnProperty(prefix = "oauth2.clients.config", name = "jdbc", havingValue = "true")
    public class AuthServerConfigByJDBC extends AuthServerConfig {

    @Autowired
    private DataSource dataSource;

    /**
    * 配置OAuth2的客户端信息:clientId、client_secret、authorization_type、redirect_url等。
    *
    * @param clients
    * @throws Exception
    */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    //方式1 也可以自定义ClientDetailsService的实现类
    //JdbcClientDetailsService detailsService = new JdbcClientDetailsService(dataSource);
    //detailsService.setPasswordEncoder(passwordEncoder);
    //clients.withClientDetails(detailsService);

    //方式2 等价于方式1
    clients.jdbc(dataSource)
    //指定密码的加密算法
    .passwordEncoder(passwordEncoder);
    log.info("OAuth2的client信息基于数据库");
    }
    }

  • 为了支持jdbc需要引入依赖

    1
    2
    3
    4
    //mysql
    runtimeOnly 'mysql:mysql-connector-java'
    //为了使用datasource
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'
  • 配置文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    spring:
    #数据源配置
    datasource:
    url: jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&useTimezone=true&serverTimezone=GMT%2B8
    username: root
    password: newpwd
    driver-class-name: com.mysql.cj.jdbc.Driver
    hikari:
    connection-timeout: 30000 #毫秒,默认30秒
    idle-timeout: 600000 #毫秒,默认10分钟
    max-lifetime: 1800000 #毫秒,默认30分钟
    maximum-pool-size: 10 #默认10
  • 数据表

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    -- oauth2中规定的数据表,需要手动创建,一般项目中提供服务接口插入,参数由用户定义,在请求时会自动查询服务器中对应的参数数据匹配认证
    # 客户端注册信息表
    CREATE TABLE `oauth_client_details` (
    `client_id` varchar(256) NOT NULL COMMENT '客户端的id',
    `resource_ids` varchar(256) DEFAULT NULL COMMENT '资源服务器的id,多个用,(逗号)隔开',
    `client_secret` varchar(256) DEFAULT NULL COMMENT '客户端的秘钥,需要PasswordEncoder加密',
    `scope` varchar(256) DEFAULT NULL COMMENT '客户端的访问范围,多个逗号分隔',
    `authorized_grant_types` varchar(256) DEFAULT NULL COMMENT '认证的方式,可选值 授权码模式:authorization_code,密码模式:password,刷新token: refresh_token, 隐式模式: implicit: 客户端模式: client_credentials。支持多个用逗号分隔',
    `web_server_redirect_uri` varchar(256) DEFAULT NULL COMMENT '授权码模式认证成功跳转的地址。authorization_code和implicit需要该值进行校验,注册时填写,多个逗号分隔',
    `authorities` varchar(256) DEFAULT NULL,
    `access_token_validity` int(11) DEFAULT NULL COMMENT 'access_token的有效时间(秒),默认(60 * 60 * 12,12小时)',
    `refresh_token_validity` int(11) DEFAULT NULL COMMENT 'refresh_token有效期(秒),默认(60 *60 * 24 * 30, 30天)',
    `additional_information` varchar(4096) DEFAULT NULL COMMENT '附加信息,值必须是json格式',
    `autoapprove` varchar(256) DEFAULT NULL COMMENT '默认false,适用于authorization_code模式,设置用户是否自动approval操作,设置true跳过用户确认授权操作页面,直接跳到redirect_uri,也可以这只scope中设置的值,表示只有这个scope会跳过授权页面,多个scope逗号分隔',
    PRIMARY KEY (`client_id`) USING BTREE
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC;

    # 初始化数据,密码要经过PasswordEncoder加密
    INSERT INTO `oauth_client_details` VALUES ('postman', NULL, '$2a$10$Owubqs9VaN.vmskZ2B0UTe0GmOMwgTmhGtlIFBjrfCz2glBqISHSu', 'any,all', 'authorization_code,refresh_token,implicit,password,client_credentials', 'http://localhost:8080/redirect,http://localhost:8088/postman/login', NULL, 42300, 2592000, NULL, 'any');
    INSERT INTO `oauth_client_details` VALUES ('demo-client', NULL, '$2a$10$v/B9.6c9NUXFbJDHqc28he6VWeyJNOBOD1UI7bwBDfBZTwY4zzcda', 'any', 'authorization_code,refresh_token,password', 'http://localhost:8080/redirect', NULL, 42300, 2592000, NULL, '');

  • 为了方便在内存和jdbc中切换,增加了自定义属性

    1
    2
    3
    4
    5
    #oauth2的client信息是基于内存还是基于数据库,如果不配置,默认为基于内存
    oauth2:
    clients:
    config:
    jdbc: true

jks证书创建

生成jks证书

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 默认证书有效期3个月
$ keytool -genkeypair -alias oauth2 -keyalg RSA -keysize 2048 -keypass 123456 -keystore oauth2_key.jks -storepass 123456
# 指定证书有效期,这里设置有效期大约10年
$ keytool -genkeypair -alias oauth2 -keyalg RSA -keysize 2048 -validity 36500 -keypass 123456 -keystore oauth2_key.jks -storepass 123456

您的名字与姓氏是什么?
[Unknown]: han
您的组织单位名称是什么?
[Unknown]: han
您的组织名称是什么?
[Unknown]: han
您所在的城市或区域名称是什么?
[Unknown]: bj
您所在的省/市/自治区名称是什么?
[Unknown]: bj
该单位的双字母国家/地区代码是什么?
[Unknown]: zh
CN=han, OU=han, O=han, L=bj, ST=bj, C=zh是否正确?
[否]: y


Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore oauth2_key.jks -destkeystore oauth2_key.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。


# 按照警告执行如下命令
$ keytool -importkeystore -srckeystore oauth2_key.jks -destkeystore oauth2_key.jks -deststoretype pkcs12
输入源密钥库口令:
已成功导入别名 oauth2 的条目。
已完成导入命令: 1 个条目成功导入, 0 个条目失败或取消

Warning:
已将 "oauth2_key.jks" 迁移到 Non JKS/JCEKS。将 JKS 密钥库作为 "oauth2_key.jks.old" 进行了备份。

导出公钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
$ keytool -list -rfc --keystore oauth2_key.jks | openssl x509 -inform pem -pubkey
输入密钥库口令: 123456
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmVdbw3XtFamdasjItlko
m8SyJiH0DnSUm1tqJDHY9orHA+0oa+VAlvxiHHBegKMX6FmCX3HAoVzHuAlWAZp0
FyV0SUR4/tuOKOw/N7HbKGa/JZJSfaNdJAEobRxzd8woaLNlCRLelzDPhMy9kGtp
x+Kc60smeo6XpC7RT25Mf5DRvKRJo4RGbPQNj18hWKZtY/TFZYySpa57eI9VOM5u
fvWJkh3Jm5cOXHiHScmF4mdNATR3XQTHXD+TDu0rLgn7H4ap9uYDRTNVGVJ/JfYu
aCrzszMFt4b+JNxz1UL42tTgbtKj8TxUrTRGTI/7KiwD5wjtpISSxlqoK1c0qgCh
KQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDQTCCAimgAwIBAgIEZ0BO+jANBgkqhkiG9w0BAQsFADBRMQswCQYDVQQGEwJ6
aDELMAkGA1UECBMCYmoxCzAJBgNVBAcTAmJqMQwwCgYDVQQKEwNoYW4xDDAKBgNV
BAsTA2hhbjEMMAoGA1UEAxMDaGFuMB4XDTIwMTEwODEyNDI0M1oXDTIxMDIwNjEy
NDI0M1owUTELMAkGA1UEBhMCemgxCzAJBgNVBAgTAmJqMQswCQYDVQQHEwJiajEM
MAoGA1UEChMDaGFuMQwwCgYDVQQLEwNoYW4xDDAKBgNVBAMTA2hhbjCCASIwDQYJ
KoZIhvcNAQEBBQADggEPADCCAQoCggEBAJlXW8N17RWpnWrIyLZZKJvEsiYh9A50
lJtbaiQx2PaKxwPtKGvlQJb8YhxwXoCjF+hZgl9xwKFcx7gJVgGadBcldElEeP7b
jijsPzex2yhmvyWSUn2jXSQBKG0cc3fMKGizZQkS3pcwz4TMvZBracfinOtLJnqO
l6Qu0U9uTH+Q0bykSaOERmz0DY9fIVimbWP0xWWMkqWue3iPVTjObn71iZIdyZuX
Dlx4h0nJheJnTQE0d10Ex1w/kw7tKy4J+x+GqfbmA0UzVRlSfyX2Lmgq87MzBbeG
/iTcc9VC+NrU4G7So/E8VK00RkyP+yosA+cI7aSEksZaqCtXNKoAoSkCAwEAAaMh
MB8wHQYDVR0OBBYEFKM+xW8pT4EHc+5/svExkEKw8c3CMA0GCSqGSIb3DQEBCwUA
A4IBAQB3OHZ1BrT+2hUFxhAk7K/7hC8tHVF8iRXhGBdnZWHNz2GRfzxOeyS4Amkp
wCixoKg9GPhUL1fxBya7Z7wjn4jS5f9b5q40c/fP23zIrmbJ3rsqlMnx3vqrcnJB
KF1l9i9h4iLDoTSS1HC42CuPSCSV/4/g2zXrZPWMVMHPHM9Ul8q+aTE7VaRzRsf8
h7xCeqZXRg2z+wFHH4B1LMcfOHwpGWVJ46xgNqt3cx4IovVTcuXbcQGKgd2+/UuQ
vugI0kWUCOH+CME9wbncIEJlDT1510GJIyt4EWyU3nio+vsLnlKz2jRP7QSE5dxY
ps3Y1u9/nrBl1yK4+KEFAguUikbp
-----END CERTIFICATE-----
  • 这段就是公钥,保存到文件即可
    1
    2
    3
    4
    5
    6
    7
    8
    9
    -----BEGIN PUBLIC KEY-----
    MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAmVdbw3XtFamdasjItlko
    m8SyJiH0DnSUm1tqJDHY9orHA+0oa+VAlvxiHHBegKMX6FmCX3HAoVzHuAlWAZp0
    FyV0SUR4/tuOKOw/N7HbKGa/JZJSfaNdJAEobRxzd8woaLNlCRLelzDPhMy9kGtp
    x+Kc60smeo6XpC7RT25Mf5DRvKRJo4RGbPQNj18hWKZtY/TFZYySpa57eI9VOM5u
    fvWJkh3Jm5cOXHiHScmF4mdNATR3XQTHXD+TDu0rLgn7H4ap9uYDRTNVGVJ/JfYu
    aCrzszMFt4b+JNxz1UL42tTgbtKj8TxUrTRGTI/7KiwD5wjtpISSxlqoK1c0qgCh
    KQIDAQAB
    -----END PUBLIC KEY-----

查看密钥信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
$ keytool -list -v -keystore oauth2_key.jks
输入密钥库口令:
密钥库类型: PKCS12
密钥库提供方: SUN

您的密钥库包含 1 个条目

别名: oauth2
创建日期: 2020-11-9
条目类型: PrivateKeyEntry
证书链长度: 1
证书[1]:
所有者: CN=han, OU=han, O=han, L=bj, ST=bj, C=zh
发布者: CN=han, OU=han, O=han, L=bj, ST=bj, C=zh
序列号: 67404efa
有效期为 Sun Nov 08 20:42:43 CST 2020 至 Sat Feb 06 20:42:43 CST 2021
证书指纹:
MD5: 3E:15:19:80:91:10:00:A8:63:A6:5E:19:5A:0E:A9:E5
SHA1: E8:BB:68:99:7E:33:6D:51:40:EF:C0:AC:91:A6:93:15:ED:FE:F8:3A
SHA256: CE:C7:C3:BF:BB:94:28:64:1B:EC:1C:F3:A9:DC:40:C5:53:AD:F2:27:01:83:8D:37:90:E0:EB:DB:C9:73:A5:5C
签名算法名称: SHA256withRSA
主体公共密钥算法: 2048 位 RSA 密钥
版本: 3

扩展:

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: A3 3E C5 6F 29 4F 81 07 73 EE 7F B2 F1 31 90 42 .>.o)O..s....1.B
0010: B0 F1 CD C2 ....
]
]



*******************************************
*******************************************

OAuth2支持获取access_token的方式

  • authorization_code 验证码模式
  • implicit 隐式模式
  • password 密码模式
  • client_credentials 客户端模式
  • refresh_token 刷新token模式

Oauth2提供的默认端点(endpoints)

1
2
3
4
5
6
/oauth/authorize:授权端点 AuthorizationEndpoint 按照OAuth 2.0规范的授权实现
/oauth/token:令牌端点 TokenEndpoint 按照OAuth 2.0规范请求Token
/oauth/confirm_access:用户确认授权提交端点
/oauth/error:授权服务错误信息端点
/oauth/check_token:用于资源服务访问的令牌解析端点 CheckTokenEndpoint 解码Client的Access Token用来检查和确认生成的Token
/oauth/token_key:提供公有密匙的端点,如果使用JWT令牌的话 TokenKeyEndpoint 提供JWT编码的Token

验证码模式–authorization_code,最常用的模式

  1. 获取验证码

    1
    2
    浏览器GET
    http://localhost:8080/oauth/authorize?client_id=postman&response_type=code&redirect_uri=http://localhost:8080/redirect
    • 1.1 参数说明
      1
      2
      3
      4
      response_type=code #授权码认证,固定值
      client_id=postman #客户端id
      redirect_uri=http://localhost:8080/redirect #重定向url,这个值要与配置的值一致,配置这个值时可以配置一个不存在的路径
      scope=any 可选参数,指定请求范围,如果不希望跳转到确认页面而是直接通过,需要在代码中配置autoApprove(true)或者autoApprove("any")
    • 1.2 跳转到登录页面,使用admin/123456登录
    • 1.3 登录成功后跳转到认证确认页面(如果不希望跳转到确认页面而是直接通过,需要在代码中配置autoApprove(true)),点击Authorize按钮,页面会跳转到http://localhost:8080/redirect?code=7ak4gI
    • 1.4 复制code的值进入第二步
  2. 获取access_token和refresh_token

    1
    2
    POST
    http://localhost:8080/oauth/token?grant_type=authorization_code&client_id=postman&client_secret=postman&redirect_uri=http://localhost:8080/redirect&code=7ak4gI
    • 2.1 code存在有效期,且只能使用一次,所以这个请求只能使用一次,参数说明
      1
      2
      3
      4
      5
      grant_type=authorization_code #验证code并获取access_token,固定值
      client_id=postman #客户端id
      client_secret=postman #客户端密码
      redirect_uri=http://localhost:8080/redirect #重定向url
      code=7ak4gI #上一步中获取到的code值
    • 2.2 响应结果如下,默认access_token有效期12小时,refresh_token有效期30天
      1
      2
      3
      4
      5
      6
      7
      8
      9
      {
      "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImV4cCI6MTYwNDYxMzI3MywiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJhNzFhMDJhYy02NzgyLTRiODEtOGJiMi1mMGI3ZWVkODk2YTgiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.SKl-0KbdVvv3acqLlXre66PX-fFIruTLwOkCO3peby0",
      "token_type": "bearer",
      "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImF0aSI6ImE3MWEwMmFjLTY3ODItNGI4MS04YmIyLWYwYjdlZWQ4OTZhOCIsImV4cCI6MTYwNzE2MjA3MywiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI5NzRjMDRkOS03NTQ0LTRjZTEtODMzZS05NjlmODMyNmRiNmQiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.vy16MpRikSFFz_n2zJEgI5Cfy1APNDrvnuP7Sj1jqJU",
      "expires_in": 43199,
      "scope": "any",
      "jwt-ext": "JWT 扩展信息",
      "jti": "a71a02ac-6782-4b81-8bb2-f0b7eed896a8"
      }
    • 2.3 复制refresh_token的值进入第三步
    • 2.4 请求时支持base认证方式,详见3.3
  3. 通过refresh_token获取新的access_token

    1
    2
    POST
    http://localhost:8080/oauth/token?grant_type=refresh_token&client_id=postman&client_secret=postman&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImF0aSI6ImE3MWEwMmFjLTY3ODItNGI4MS04YmIyLWYwYjdlZWQ4OTZhOCIsImV4cCI6MTYwNzE2MjA3MywiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI5NzRjMDRkOS03NTQ0LTRjZTEtODMzZS05NjlmODMyNmRiNmQiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.vy16MpRikSFFz_n2zJEgI5Cfy1APNDrvnuP7Sj1jqJU
    • 3.1 参数说明,这个就是refresh_token模式,需要先获取到上一次的refresh_token

      1
      2
      3
      4
      grant_type=refresh_token #刷新token的参数,固定值
      client_id=postman #客户端id
      client_secret=postman #客户端密码
      refresh_token=xxx #上一步中获取的refresh_token值
    • 3.2 响应结果如下,获取到新的access_token和refresh_token,注意保存,access_token过期时,可以通过该请求重新获得新的access_token

      1
      2
      3
      4
      5
      6
      7
      8
      9
      {
      "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImV4cCI6MTYwNDYxMzc4NSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI5YWQ1MTBhNy00MzQyLTQ5M2UtYjQyMS1iNzNmMDU2NmU3OWYiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.L0TPZ-NDzT4hSSivQ0WR-5rPz6xoLbC_HmdvvKOzP_Y",
      "token_type": "bearer",
      "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImF0aSI6IjlhZDUxMGE3LTQzNDItNDkzZS1iNDIxLWI3M2YwNTY2ZTc5ZiIsImV4cCI6MTYwNzE2MjA3MywiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI5NzRjMDRkOS03NTQ0LTRjZTEtODMzZS05NjlmODMyNmRiNmQiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.ekAST3MRc0PqW3FZgnFQv1g-HykCqA4_TuzLRTplCIM",
      "expires_in": 43199,
      "scope": "any",
      "jwt-ext": "JWT 扩展信息",
      "jti": "9ad510a7-4342-493e-b421-b73f0566e79f"
      }
    • 3.3 请求时支持base认证方式

      1
      2
      # 将client_id和client_secret放到url的前面
      POST http://postman:postman@localhost:8080/oauth/token?grant_type=refresh_token&refresh_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImF0aSI6ImM4NWM0YjJlLTBlNTktNDJhYy05M2RlLWI2N2I0OWI1ZDU5OSIsImV4cCI6MTYwNzIyMDAxMiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI2M2U2MGYxOS1iOTU1LTRlMmUtODk2Yy1mYWUxN2IyODUzMzkiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.jVZWYRjr4VwTYwbKXP-sav_08vwc8iAxzEbg0I_jVTw

      或者在header中增加Authorization请求头,值为Basic xxxxxxxxxxxxxxxxxx是通过base64.encode(client_id + 空格 + client_secret)得到。
      使用Postman接口测试工具时,也可以使用其提供的认证功能[Authorization–>TYPE–>Basic Auth],直接填写用户名和密码就可以方便进行测试,注意这里用户名和密码是client_id 和 client_secret。

      1
      2
      3
      4
      5
      6
      7
      8
      9
      {
      "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImV4cCI6MTYwNDY4NTE3NiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJhNDhiNzZhNC1kNWM0LTRkNTYtYmQ2Yy0zOWYxODQ0MGYwNTMiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.mhNUhWk1M9p267bOsv3fYXnLGeXitvnny6mUT9FyNdw",
      "token_type": "bearer",
      "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImF0aSI6ImE0OGI3NmE0LWQ1YzQtNGQ1Ni1iZDZjLTM5ZjE4NDQwZjA1MyIsImV4cCI6MTYwNzIyMDAxMiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI2M2U2MGYxOS1iOTU1LTRlMmUtODk2Yy1mYWUxN2IyODUzMzkiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.DT_cBdcow7pDv_S9RTHknKLHxZAIT45Byw1Rdw93K8U",
      "expires_in": 43199,
      "scope": "any",
      "jwt-ext": "JWT 扩展信息",
      "jti": "a48b76a4-d5c4-4d56-bd6c-39f18440f053"
      }
    • 3.4 access_token信息

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      //HEADER
      {
      "alg": "HS256",
      "typ": "JWT"
      }
      //PAYLOAD
      {
      "user_name": "admin",
      "jwt-ext": "JWT 扩展信息",
      "scope": [
      "any"
      ],
      "exp": 1604613785,
      "authorities": [
      "ROLE_admin"
      ],
      "jti": "9ad510a7-4342-493e-b421-b73f0566e79f",
      "client_id": "postman"
      }

implicit模式

  • 仅可获取access_token,不能获取refresh_token
  • 跳转到登录页面
    1
    2
    浏览器GET
    http://localhost:8080/oauth/authorize?client_id=postman&response_type=token&redirect_uri=http://localhost:8080/redirect
  • 然后登录,然后同意授权
  • 返回结果直接拼接在redirect_uri的后面,因为是以#开头,所以服务器端无法收到数据,只能从浏览器中获取了
    1
    http://localhost:8080/redirect#access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImV4cCI6MTYwNDY3NTMwNSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI4ZDcyODM5NC0zODhjLTRiMjAtYjgwYy1jNTk4OWJjZmJlM2IiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.OStAhkT6fBoNLdF7vTaztwhfNV5J8K1MpE1A7pxbyCs&token_type=bearer&expires_in=43199&scope=any&jwt-ext=JWT%20%E6%89%A9%E5%B1%95%E4%BF%A1%E6%81%AF&jti=8d728394-388c-4b20-b80c-c5989bcfbe3b
  • access_token信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //HEADER
    {
    "alg": "HS256",
    "typ": "JWT"
    }
    //PAYLOAD
    {
    "user_name": "admin",
    "jwt-ext": "JWT 扩展信息",
    "scope": [
    "any"
    ],
    "exp": 1604675305,
    "authorities": [
    "ROLE_admin"
    ],
    "jti": "8d728394-388c-4b20-b80c-c5989bcfbe3b",
    "client_id": "postman"
    }

password 模式

  • 密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向”服务商提供商”索要授权。
  • 在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
  • 获取access_token和refresh_token
    1
    2
    POST
    http://localhost:8080/oauth/token?username=admin&password=123456&grant_type=password&client_id=postman&client_secret=postman
  • 请求时支持base认证方式,认证用户名和密码是client_id 和 client_secret
  • grant_type=password,需要携带用户的用户名和密码
  • 返回结果
    1
    2
    3
    4
    5
    6
    7
    8
    9
    {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImV4cCI6MTYwNDY3NzU1MiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiJlNjZiMWFjOS04M2RmLTRiNTMtYjNmYS1mMTQyYzA2MTYzMjkiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.jfBeoA-AeNaMUSWHKBktN8IOFbGJHtQSIQUgxP8zzg0",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55Il0sImF0aSI6ImU2NmIxYWM5LTgzZGYtNGI1My1iM2ZhLWYxNDJjMDYxNjMyOSIsImV4cCI6MTYwNzIyNjM1MiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiI5MDUwNTQ0Yi00OGVmLTQ2OWMtYjM3OS1lMWQyZDViOTcyYjkiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.fGDixRXD68cYdRTjprH3TM7gAGmFqDAaKxs3RTxphMs",
    "expires_in": 43199,
    "scope": "any",
    "jwt-ext": "JWT 扩展信息",
    "jti": "e66b1ac9-83df-4b53-b3fa-f142c0616329"
    }
  • access_token信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    //HEADER
    {
    "alg": "HS256",
    "typ": "JWT"
    }
    //PAYLOAD
    {
    "user_name": "admin",
    "jwt-ext": "JWT 扩展信息",
    "scope": [
    "any"
    ],
    "exp": 1604677552,
    "authorities": [
    "ROLE_admin"
    ],
    "jti": "e66b1ac9-83df-4b53-b3fa-f142c0616329",
    "client_id": "postman"
    }

client_credentials 客户端模式

  • 客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向”服务提供商”进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求”服务提供商”提供服务,其实不存在授权问题。
  • 获取access_token
    1
    2
    POST
    http://localhost:8080/oauth/token?grant_type=client_credentials&client_id=postman&client_secret=postman
  • 不需要用户的用户名和密码,只是认证客户端是否有效,返回的access_token的payload中也不包含任何用户信息(user_name,authorities)
  • 没有refresh_token
  • 返回结果
    1
    2
    3
    4
    5
    6
    7
    8
    {
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqd3QtZXh0IjoiSldUIOaJqeWxleS_oeaBryIsInNjb3BlIjpbImFueSJdLCJleHAiOjE2MDQ2Nzc4NjksImp0aSI6ImU3ZjJhYTEyLTFhMWUtNGFmMC04MjJkLTkxNzg5NmYyMGMwMyIsImNsaWVudF9pZCI6InBvc3RtYW4ifQ.OHy0IUGf9KSmCDKlqq1IZ8bICAhBHFtpazwK1gcbCOI",
    "token_type": "bearer",
    "expires_in": 43199,
    "scope": "any",
    "jwt-ext": "JWT 扩展信息",
    "jti": "e7f2aa12-1a1e-4af0-822d-917896f20c03"
    }
  • access_token信息
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    //HEADER
    {
    "alg": "HS256",
    "typ": "JWT"
    }
    //PAYLOAD
    {
    "jwt-ext": "JWT 扩展信息",
    "scope": [
    "any"
    ],
    "exp": 1604677869,
    "jti": "e7f2aa12-1a1e-4af0-822d-917896f20c03",
    "client_id": "postman"
    }

refresh_token模式,见验证码模式第3部分

通过已有的refresh_token获取新的access_token和refresh_token

验证token有效性–Basic Auth

1
POST http://postman:postman@localhost:8080/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsImp3dC1leHQiOiJKV1Qg5omp5bGV5L-h5oGvIiwic2NvcGUiOlsiYW55IiwiYWxsIl0sImV4cCI6MTYwNDcxNjMyMCwiYXV0aG9yaXRpZXMiOlsiUk9MRV9hZG1pbiJdLCJqdGkiOiIxNjc4ZTNhYy0yYmNjLTQ4NGUtOTBkMy02ZWFjYTVkNzkyM2IiLCJjbGllbnRfaWQiOiJwb3N0bWFuIn0.8piTJdoAf-4lB0Tn-yeSFWoV3WpkJkqBs7AsoNPoohw

或者加上Header参数Authorization 值为 Basic cG9zdG1hbjpwb3N0bWFu

返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"user_name": "admin",
"jwt-ext": "JWT 扩展信息",
"scope": [
"any"
],
"active": true,
"exp": 1604767911,
"authorities": [
"ROLE_admin"
],
"jti": "5597b675-a617-4e02-959f-86d62024a237",
"client_id": "postman"
}

获取公钥,比如JWT的密钥

1
GET http://postman:postman@localhost:8080/oauth/token_key

或者加上Header参数Authorization 值为 Basic cG9zdG1hbjpwb3N0bWFu

返回值

1
2
3
4
{
"alg": "HMACSHA256",
"value": "Ayl7bn+aFwxlakekKCJiqUYguKS80bEVb7OZtd2qfZjdCbAwKxDmM6PWezGy5JIkiJfemtHNPc7Av1l+OWQSqQ=="
}

自定义登录和授权页面

依赖

1
2
3
4
5
6
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
//webjars https://www.webjars.org
implementation 'org.webjars:bootstrap:4.5.3'
implementation 'org.webjars.bower:jquery:3.5.1'
// 可以在html中去掉webjars的版本号,这样升级的时候直接修改上面引入的webjars中的版本号即可,页面中不需要修改
implementation 'org.webjars:webjars-locator:0.40'

OAuth2Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package com.example.oauth2authserverdemo.controller;

import com.sun.istack.internal.Nullable;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.SessionAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Controller
@SessionAttributes({"authorizationRequest"})
public class OAuth2Controller {

/**
* 自定义授权页面,注意:一定要在类上加@SessionAttributes({"authorizationRequest"})
*
* @param model model
* @param request request
* @return String
* @throws Exception Exception
*/
@RequestMapping("/oauth/confirm_access")
public String getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) {

List<String> scopeList = new ArrayList<>();

String scope = request.getParameter("scope");
if (scope != null) {
String[] split = scope.split(" ");
for(String s:split) {
scopeList.add(s);
}
}


model.put("scopeList", scopeList);
return "oauth2/confirm_access";
}

/**
* <h2>自定义登录</h2>
* Created by hanqf on 2020/11/13 10:13. <br>
*
* @return java.lang.String
* @author hanqf
*/
@RequestMapping("/oauth/login")
public String login(Model model,@Nullable Boolean error,HttpServletRequest request) {
if(error!=null){
model.addAttribute("error",error);
}else{
//从客户端logout后,第一次重新登录会不成功,这里加一个处理逻辑,如果发现其queryString的值为logout就重定向到首页
//暂时不清楚客户端登出后该如何设置才能直接重新登录客户端,可以在认证服务器首页增加客户端入口
if("logout".equals(request.getQueryString())){
return "redirect:/";
}
}
return "oauth2/login";
}

}

login.html

  • 登录页面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>登录</title>

    <link rel="stylesheet" th:href="@{/webjars/bootstrap/css/bootstrap.min.css}"/>

    <script th:src="@{/webjars/jquery/jquery.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/js/bootstrap.min.js}"></script>

    </head>
    <body>


    <div class="container" style="width: 320px;margin-top: 100px;">
    <h3 align="center">OAuth2登录</h3>
    <p class="alert-danger" style="text-align: center" th:if="${error}">用户名或密码错误!</p>
    <p id="notice_null" class="alert-danger" style="text-align: center;">用户名或密码不能为空!</p>
    <form th:action="@{/login}" method="post" onsubmit="return onsubmitCheck()">
    <table class="table">
    <tr>
    <td>用户名:</td>
    <td>
    <label><input type="text" id="username" name="username"/></label>
    </td>
    </tr>
    <tr>
    <td>密码:</td>
    <td>
    <label><input type="password" id="password" name="password"/></label>
    </td>
    </tr>

    <tr>
    <td></td>
    <td>
    <button class="btn-success" type="submit">登录</button>
    </td>
    </tr>
    </table>
    </form>

    </div>

    <script type="text/javascript">
    $(function(){
    //默认不显示错误提醒
    $("#notice_null").hide();
    });

    function onsubmitCheck(){
    var username = $("#username").val();
    var password = $("#password").val();

    if(username == "" || password == ""){
    $("#notice_null").show();
    return false;
    }

    return true;
    }

    </script>
    </body>
    </html>

confirm_access.html

  • 授权页面
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
    <meta charset="UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <title>确认授权页面</title>
    <META http-equiv="Pragma" content="no-cache">
    <META http-equiv="Cache-Control" content="no-cache">
    <META http-equiv="Expire" content="0">

    <link rel="stylesheet" th:href="@{/webjars/bootstrap/css/bootstrap.min.css}" />

    <script th:src="@{/webjars/jquery/jquery.min.js}"></script>
    <script th:src="@{/webjars/bootstrap/js/bootstrap.min.js}"></script>

    </head>
    <body>
    <div class="container">
    <section class="alert alert-warning">
    <p>是否授权[ <span th:text="${session.authorizationRequest.clientId}">clientId</span> ]的访问您的资源:</p>
    <div class="alert alert-info">

    <form id='confirmationForm' name='confirmationForm' action="/oauth/authorize" method='post'>
    <input id="user_oauth_approval" name='user_oauth_approval' value='true' type='hidden'/>
    <!--授权访问领域-->
    <span th:text="${'['+s+']'}" th:each="s : ${scopeList}"/>
    <input class="btn-success" id="authorize" name='authorize' value='授权' type='submit'/>
    <input class="btn-primary" id="refuse" name='refuse' value='拒绝' type='button'/>
    </form>
    </div>
    </section>
    </div>
    </body>

    <script type="text/javascript">
    $(function(){
    $("#refuse").click(function(){
    $("#user_oauth_approval").val('false');
    $("#confirmationForm").submit();
    });
    });

    </script>
    </html>

    SecurityConfig中开启自定义登录页配置

    1
    2
    3
    4
    5
    6
    http.formLogin()
    .loginPage("/oauth/login")
    .loginProcessingUrl("/login")
    //.defaultSuccessUrl("/")
    .failureForwardUrl("/oauth/login?error=true")
    .permitAll(); //登记界面,默认是permitAll

    后记

    至此,一个基于SpringBoot-OAuth2-JWT的认证服务器就搭建好了,如果client-server和resource-server都是基于spring-security-oauth2搭建的,则认证服务器就不需要其它配置了,client-server和resource-server的代码可以参考
    oauth2-client-demooauth2-resource-server-demo,不过spring-security-oauth2已经不再维护了,所以建议搭建client-server和resource-server的时候,还是使用springboot官方提供的spring-boot-starter-oauth2-clientspring-boot-starter-oauth2-resource-server,此时还需要为认证服务增加一些功能,这部分留到后面讲解client-server和resource-server的搭建的时候再说。
---------------- The End ----------------

欢迎关注我的其它发布渠道