1. 首页
  2. >
  3. 编程技术
  4. >
  5. Java

SpringBoot2 整合OAuth2实现统一认证

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

环境:2.2.11.RELEASE + 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>
  • 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类
public interface AppRepository extends JpaRepository<App, String>, JpaSpecificationExecutor<App> {  	App findByClientId(String clientId) ; 	 } 提供了一个方法,根据clientId获取客户端信息。
public interface UsersRepository extends JpaRepository<Users, String>, JpaSpecificationExecutor<Users> {  	Users findByUsernameAndPassword(String username, String password) ; 	 } 登录方法
  • 核心配置类

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

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

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

@Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter {       @Override     @Bean     public AuthenticationManager authenticationManagerBean() throws Exception {         return super.authenticationManagerBean();     } }
  • 自定义ClientDetails

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


SpringBoot2 整合OAuth2实现统一认证

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

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 ; }
  • 登录认证类
@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)); 	} 	 }
  • 密码验证
@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) ; 	}  }

注意:

Users实体类为啥要实现UserDetails?

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

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


SpringBoot2 整合OAuth2实现统一认证


SpringBoot2 整合OAuth2实现统一认证

进入上面的方法中:


SpringBoot2 整合OAuth2实现统一认证

进入getName方法中:


SpringBoot2 整合OAuth2实现统一认证

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


SpringBoot2 整合OAuth2实现统一认证

这里是通过debug查看


到此整合完毕了!

测试:

先造两条数据:


SpringBoot2 整合OAuth2实现统一认证


SpringBoot2 整合OAuth2实现统一认证

  • 授权码模式

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

SpringBoot2 整合OAuth2实现统一认证

请求地址


SpringBoot2 整合OAuth2实现统一认证

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

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


SpringBoot2 整合OAuth2实现统一认证

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


SpringBoot2 整合OAuth2实现统一认证

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

  • 密码模式

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

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

SpringBoot2 整合OAuth2实现统一认证

请求地址


SpringBoot2 整合OAuth2实现统一认证

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


SpringBoot2 整合OAuth2实现统一认证

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


SpringBoot2 整合OAuth2实现统一认证

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

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

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

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

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

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

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

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

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


SpringBoot2 整合OAuth2实现统一认证

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

完毕!!!

SpringBoot2 整合 OAuth2 资源认证(保护)
« 上一篇 2020年11月25日 am07:02
vue-router从零开始
下一篇 » 2020年11月25日 am07:48

相关推荐