关于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是为了简化使用的。如下图:
这里就是为了获取当前客户端的所有信息使用。
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生成方式:
进入上面的方法中:
进入getName方法中:
最终它会调用红色框中的代码,这样就出现一个问题,你每次获取token的时候都会生成一个新的token。所以这里我们的Users实体实现了UserDetails接口。
这里是通过debug查看
到此整合完毕了!
测试:
先造两条数据:
- 授权码模式
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。
请求地址