SpringBoot整合OAuth2实现单点登录

开发 前端
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。

关于OAuth2不做介绍了,网络太多了。

环境:2.4.12 + OAuth2 + Redis

redis用来实现token的存储。

  • pom.xml
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-pool2</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security.oauth.boot</groupId>
  <artifactId>spring-security-oauth2-autoconfigure</artifactId>
  <version>2.2.11.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
  <groupId>net.sourceforge.nekohtml</groupId>
  <artifactId>nekohtml</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
  • 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.
  • application.yml
server:
  port: 8208
---
spring:
  application:
    name: oauth-server
---
spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 1
    lettuce:
      pool:
        maxActive: 8
        maxIdle: 100
        minIdle: 10
        maxWait: -1
---
spring:
  resources:
    staticLocations: classpath:/static/,classpath:/templates/,classpath:/pages/
  mvc:
    staticPathPattern: /resources/**
---
spring:
  datasource:
    driverClassName: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/test?serverTimezone=GMT%2B8
    username: root
    password: 123456
    type: com.zaxxer.hikari.HikariDataSource
    hikari:
      minimumIdle: 10
      maximumPoolSize: 200
      autoCommit: true
      idleTimeout: 30000
      poolName: MasterDatabookHikariCP
      maxLifetime: 1800000
      connectionTimeout: 30000
      connectionTestQuery: SELECT 1    
  jpa:
    hibernate:
      ddlAuto: update
    showSql: true
    openInView: true #Open EntityManager in View
---
spring:
  thymeleaf:
    servlet:
      contentType: text/html; charset=utf-8 
    cache: false
    mode: LEGACYHTML5
    encoding: UTF-8
    enabled: true
    prefix: classpath:/pages/
    suffix: .html
---
spring:
  main:
    allow-bean-definition-overriding: true
  • 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.
  • 实体
@Entity
@Table(name = "T_APP")
public class App implements Serializable {


  private static final long serialVersionUID = 1L ;
  @Id
  @GeneratedValue(generator = "system-uuid")
  @GenericGenerator(name = "system-uuid", strategy = "uuid")
  private String id ;
  /**
   * 客户端ID
   */
  private String clientId ;
  /**
   * 客户端密钥
   */
  private String clientSecret ;
  /**
   * 跳转地址
   */
  private String redirectUri ;
}
// 该实体用来存在每个应用的信息。
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
@Entity
@Table(name = "T_USERS")
public class Users implements UserDetails, Serializable {


  private static final long serialVersionUID = 1L;


  @Id
  @GeneratedValue(generator = "system-uuid")
  @GenericGenerator(name = "system-uuid", strategy = "uuid")
  private String id ;
  private String username ;
  private String password ;
}
// 该实体是用户登录信息。
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • DAO类
// 提供了一个方法,根据clientId获取客户端信息。
public interface AppRepository extends JpaRepository<App, String>, JpaSpecificationExecutor<App> {




  App findByClientId(String clientId) ;
  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
public interface UsersRepository extends JpaRepository<Users, String>, JpaSpecificationExecutor<Users> {


  Users findByUsernameAndPassword(String username, String password) ;
  
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 核心配置类

重要代码已经加了注释说明

@Configuration
@EnableAuthorizationServer
public class OAuthAuthorizationConfig extends AuthorizationServerConfigurerAdapter {


  @Resource
  private AppRepository appRepository ;
  @Resource
  private RedisConnectionFactory redisConnectionFactory ;
  @Resource
  private AuthenticationManager authenticationManager;
  
  @Override
  public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    clients.withClientDetails(clientDetailsService());
  }
  
  @Override
  public void configure(AuthorizationServerSecurityConfigurer security)
      throws Exception {
   security.tokenKeyAccess("permitAll()") // isAuthenticated()
     .checkTokenAccess("permitAll()") // 允许访问 /oauth/check_token 接口
     .allowFormAuthenticationForClients() ;
  }
  
  @Override
  public void configure(AuthorizationServerEndpointsConfigurer endpoints)
      throws Exception {
    // 自定义CODE
    endpoints.authorizationCodeServices(new InMemoryAuthorizationCodeServices() {
    @Override
    public String createAuthorizationCode(OAuth2Authentication authentication) {
      String code = UUID.randomUUID().toString().replaceAll("-", "") ;
      store(code, authentication) ;
      return code;
    }
  }) ;
    endpoints.exceptionTranslator(new DefaultWebResponseExceptionTranslator() {
      @SuppressWarnings({ "unchecked", "rawtypes" })
      @Override
      public ResponseEntity translate(Exception e) throws Exception {
        ResponseEntity<OAuth2Exception> responseEntity = super.translate(e) ;
        ResponseEntity<Map<String, Object>> customEntity = exceptionProcess(responseEntity);
        return customEntity ;
      }
    }) ;
    // 要想使用密码模式这个步骤不能少,否则默认情况下的只支持除密码模式外的其它4中模式
    endpoints.authenticationManager(authenticationManager) ;
    /**
     * 如果重新定义了TokenServices 那么token有效期等信息需要重新定义
     * 这时候在ClientDetailsServiceConfigurer中设置的有效期将会无效
     */
    endpoints.tokenServices(tokenService()) ; // 生成token的服务
    endpoints.allowedTokenEndpointRequestMethods(HttpMethod.values()) ; // 获取token 时 允许所有的方法类型
    endpoints.accessTokenConverter(defaultTokenConvert()); // token生成方式
    endpoints.tokenStore(tokenStore()) ;
    endpoints.pathMapping("/oauth/error", "/oauth/customerror") ;
    // endpoints.addInterceptor(new XXXX()) ; // 在这里可以配置拦截器
    endpoints.requestValidator(new OAuth2RequestValidator() {
      @Override
      public void validateScope(AuthorizationRequest authorizationRequest, ClientDetails client)
          throws InvalidScopeException {
        //logger.info("放行...") ;
      }
      @Override
      public void validateScope(TokenRequest tokenRequest, ClientDetails client)
          throws InvalidScopeException {
        //logger.info("放行...") ;
      }
          
    }) ;
    endpoints.approvalStore(new InMemoryApprovalStore()) ;
  }
  
  @Bean
  public ClientDetailsService clientDetailsService() {
    return (clientId) -> {
      if (clientId == null) {
        throw new ClientRegistrationException("未知的客户端: " + clientId) ;
      }
      App app = appRepository.findByClientId(clientId) ;
      if (app == null) {
        throw new ClientRegistrationException("未知的客户端: " + clientId) ;
      }
      // 因为每一个客户端都可以对应多个认证授权类型,跳转URI等信息,这里为了简单就为每一个客户端固定了这些信息
      OAuthClientDetails clientDetails = new OAuthClientDetails() ;
      clientDetails.setClientId(clientId) ;
      clientDetails.setClientSecret(app.getClientSecret()) ;
      Set<String> registeredRedirectUri = new HashSet<>() ;
      registeredRedirectUri.add(app.getRedirectUri()) ;
      clientDetails.setRegisteredRedirectUri(registeredRedirectUri);
      clientDetails.setScoped(false) ;
      clientDetails.setSecretRequired(true) ;
      clientDetails.setScope(new HashSet<String>());
      Set<String> authorizedGrantTypes = new HashSet<>() ;
      authorizedGrantTypes.add("authorization_code") ;
      authorizedGrantTypes.add("implicit") ;
      authorizedGrantTypes.add("password") ;
      authorizedGrantTypes.add("refresh_token") ;
      authorizedGrantTypes.add("client_credentials") ;
      clientDetails.setAuthorizedGrantTypes(authorizedGrantTypes);
      Collection<GrantedAuthority> authorities = new ArrayList<>() ;
      clientDetails.setAuthorities(authorities) ;
      return clientDetails ;
    } ;
  }
    
  // 如下Bean可用来增加获取Token时返回信息(需要在TokenServices中增加)
  @Bean
  public TokenEnhancer tokenEnhancer(){
    return new TokenEnhancer() {
      @Override
      public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        System.out.println(authentication) ;
        if (accessToken instanceof DefaultOAuth2AccessToken){
          DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) accessToken;
          Map<String, Object> additionalInformation = new LinkedHashMap<String, Object>();
          additionalInformation.put("username", ((Users)authentication.getPrincipal()).getUsername());
          additionalInformation.put("create_time", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
          token.setAdditionalInformation(additionalInformation);
        }
        return accessToken;
      }
    };
  }
    
  @Bean
  @Primary
  public AuthorizationServerTokenServices tokenService() {
    DefaultTokenServices tokenService = new DefaultTokenServices() ;
    tokenService.setSupportRefreshToken(true) ; // 如果不设置返回的token 将不包含refresh_token
    tokenService.setReuseRefreshToken(true) ;
    tokenService.setTokenEnhancer(tokenEnhancer()); // 在这里设置JWT才会生效
    tokenService.setTokenStore(tokenStore()) ;
    tokenService.setAccessTokenValiditySeconds(60 * 60 * 24 * 3) ; // token有效期
    tokenService.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7) ; // 30 * 24 * 60 * 60;刷新token (必须在token没有过期前使用)
    return tokenService ;
  }
    
    
  @Bean
  public TokenStore tokenStore() {
    TokenStore tokenStore = null ;
    tokenStore = new RedisTokenStore(redisConnectionFactory) ;
    return tokenStore ;
  }


  @Bean 
  public DefaultAccessTokenConverter defaultTokenConvert() {
    DefaultAccessTokenConverter defaultTokenConvert = new DefaultAccessTokenConverter() ;
    return defaultTokenConvert ;
  }
  
  private static ResponseEntity<Map<String, Object>> exceptionProcess(
        ResponseEntity<OAuth2Exception> responseEntity) {
    Map<String, Object> body = new HashMap<>() ;
    body.put("code", -1) ;
    OAuth2Exception excep = responseEntity.getBody() ;
    String errorMessage = excep.getMessage();
    if (errorMessage != null) {
      errorMessage = "认证失败,非法用户" ;
      body.put("message", errorMessage) ;
    } else {
      String error = excep.getOAuth2ErrorCode();
      if (error != null) {
        body.put("message", error) ;
      } else {
        body.put("message", "认证服务异常,未知错误") ;
      }
    }
    body.put("data", null) ;
    ResponseEntity<Map<String, Object>> customEntity = new ResponseEntity<>(body, 
    responseEntity.getHeaders(), responseEntity.getStatusCode()) ;
    return customEntity;
  }  
    
}
  • 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.
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
  • 152.
  • 153.
  • 154.
  • 155.
  • 156.
  • 157.
  • 158.
  • 159.
  • 160.
  • 161.
  • 162.
  • 163.
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
  • 171.
  • 172.
  • 173.
  • 174.
  • 175.
  • 176.
  • 177.
  • 178.
  • 暴露一个AuthenticationManager类

密码模式必须设置对应的AuthenticationManager,所以这里必须暴露出来,否则系统找不到。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
  @Override
  @Bean
  public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 自定义ClientDetails

该类主要是用在配置类中定义 ClientDetailsService是为了简化使用的。如下图:

图片图片

这里就是为了获取当前客户端的所有信息使用。

public class OAuthClientDetails implements ClientDetails,Serializable {


  private static final long serialVersionUID = 1L;


  private String id ;
  
  private String clientId ;
  
  private boolean secretRequired ;
  
  private String clientSecret ;
  
  private boolean scoped ;
  
  private Set<String> resourceIds ;
  
  private Set<String> scope = new HashSet<>();
  
  private Set<String> authorizedGrantTypes = new HashSet<>();
  
  private Set<String> registeredRedirectUri = new HashSet<>();
  
  private Collection<GrantedAuthority> authorities ;
  
  private boolean autoApprove ;
  
  private Integer accessTokenValiditySeconds ;
  
  private Integer refreshTokenValiditySeconds ;
}
  • 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.
  • 登录认证类
@Component
public class LoginAuthenticationProvider implements AuthenticationProvider {
  
  @Resource
  private UsersRepository usersRepository ;


  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    // 登录用户名
    String username = authentication.getName() ;
    // 凭证(密码)
    Object credentials = authentication.getCredentials() ;
    Users user = null ;
    try {
      user = usersRepository.findByUsernameAndPassword(username, (String) credentials) ;
      if (user == null) {
        String errorMsg = "错误的用户名或密码" ;
        throw new BadCredentialsException(errorMsg) ;
      }
    } catch (Exception e) {
      throw e ;
    }
    UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
      user, authentication.getCredentials(), Arrays.asList(
          new SimpleGrantedAuthority("ROLE_USERS"),
          new SimpleGrantedAuthority("ROLE_ACTUATOR")));
          result.setDetails(authentication.getDetails());
          return result;
      }
  
    @Override
    public boolean supports(Class<?> authentication) {
      return (UsernamePasswordAuthenticationToken.class
        .isAssignableFrom(authentication));
    }
  
}
  • 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.
  • 密码验证
@Component
public class LoginPasswordEncoder implements PasswordEncoder {
  
  @Override
  public String encode(CharSequence rawPassword) {
    return rawPassword.toString() ;
  }


  @Override
  public boolean matches(CharSequence rawPassword, String encodedPassword) {
    return this.encode(rawPassword).equals(encodedPassword) ;
  }


}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.

注意:

Users实体类为啥要实现UserDetails?

应该我们在存储token相关信息到redis时需要有对应key的生成方式。

RedisTokenStore.java中有个默认的key生成方式:

图片图片


图片图片

进入上面的方法中:

图片图片

进入getName方法中:

图片图片

最终它会调用红色框中的代码,这样就出现一个问题,你每次获取token的时候都会生成一个新的token。所以这里我们的Users实体实现了UserDetails接口。

图片图片

这里是通过debug查看

到此整合完毕了!

测试:

先造两条数据:

图片图片


图片图片

  • 授权码模式

授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。

请求地址请求地址


图片图片

访问上面地址后跳转到了登录页面

输入正确的用户名密码后:

图片图片

成功后跳到了我们配置的跳转地址,这时候我们就可以根据地址栏的code获取token了:

图片图片

注意:这里的code是一次性的,也就是说如果使用过了就会自动失效。

  • 密码模式

密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。

在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。

请求地址

图片图片

  • 客户端模式

客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于OAuth框架所要解决的问题。在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。

图片图片

  • 简化模式

简化模式(implicit grant type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。

图片图片

简化模式的流程,这样有助于理解

(A)客户端将用户导向认证服务器。

(B)用户决定是否给予客户端授权。

(C)假设用户给予授权,认证服务器将用户导向客户端指定的"重定向URI",并在URI的Hash部分包含了访问令牌。

(D)浏览器向资源服务器发出请求,其中不包括上一步收到的Hash值。

(E)资源服务器返回一个网页,其中包含的代码可以获取Hash值中的令牌。

(F)浏览器执行上一步获得的脚本,提取出令牌。

(G)浏览器将令牌发给客户端。

  • 刷新令牌

如果用户访问的时候,客户端的"访问令牌"过期前,可以申请一个新的访问令牌。

图片图片

这里的refresh_token就是在获取token的时候返回的。

责任编辑:武晓燕 来源: Spring全家桶实战案例源码
相关推荐

2023-08-31 08:34:07

Users对象序列化

2022-04-11 07:34:46

OAuth2UAA节点

2022-05-12 07:37:51

单点登录微服务开源

2024-06-20 08:20:27

2013-05-02 14:13:44

Android开发OAuth2服务认证

2017-08-04 18:10:09

2021-08-02 12:50:45

sessiontokenJava

2021-11-15 13:58:00

服务器配置授权

2021-08-29 23:33:44

OAuth2服务器Keycloak

2025-01-13 08:04:24

2016-12-26 18:05:00

单点登录原理简单实现

2020-12-28 05:52:27

SSO登录单点

2021-07-02 10:45:53

SpringBootCAS登录

2022-08-15 08:34:08

OauthCAS登录

2024-06-21 09:28:43

2024-09-11 08:37:39

2012-05-31 13:59:43

PHP

2014-09-24 11:47:41

微信企业号开发

2022-11-16 14:02:44

2012-02-14 14:17:35

ibmdw
点赞
收藏

51CTO技术栈公众号