当前位置:首页 >知识 >SpringBoot整合OAuth2实现单点登录 而不是现单以用户的名义

SpringBoot整合OAuth2实现单点登录 而不是现单以用户的名义

2024-05-16 22:18:30 [百科] 来源:避面尹邢网

SpringBoot整合OAuth2实现单点登录

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

关于OAuth2不做介绍了,整合网络太多了。现单

环境:2.4.12 + OAuth2 + Redis

SpringBoot整合OAuth2实现单点登录 而不是现单以用户的名义

redis用来实现token的点登存储。

SpringBoot整合OAuth2实现单点登录 而不是现单以用户的名义

  • 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>
  • 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
  • 实体
@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 ;}// 该实体用来存在每个应用的信息。
@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 ;}// 该实体是用户登录信息。
  • DAO类
// 提供了一个方法,点登根据clientId获取客户端信息。public interface AppRepository extends JpaRepository<App, String>, JpaSpecificationExecutor<App> {   App findByClientId(String clientId) ;  }
public interface UsersRepository extends JpaRepository<Users, String>, JpaSpecificationExecutor<Users> {   Users findByUsernameAndPassword(String username, String password) ;  }
  • 核心配置类

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

SpringBoot整合OAuth2实现单点登录 而不是现单以用户的名义

@Configuration@EnableAuthorizationServerpublic 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;  }      }
  • 暴露一个AuthenticationManager类

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

@Configurationpublic class WebSecurityConfig extends WebSecurityConfigurerAdapter {    @Override  @Bean  public AuthenticationManager authenticationManagerBean() throws Exception {     return super.authenticationManagerBean();  }}
  • 自定义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 ;}
  • 登录认证类
@Componentpublic 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));    }  }
  • 密码验证
@Componentpublic 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) ;  }}

注意:

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全家桶实战案例源码 客户端模式认证

(责任编辑:热点)

    推荐文章
    热点阅读