Because we have a microservice project based on Spring Cloud, we know about OAuth2 and plan to integrate OAuth2 to realize unified authorization. OAuth is an open network standard for authorization. The current version is 2.0. I won't introduce it here.
Development environment: Windows10, IntelliJ idea2018 2, jdk1.8, redis3.2.9, Spring Boot 2.0.2 Release, Spring Cloud Finchley.RC2 Spring 5.0.6
Project directory
eshop -- parent project, managing jar package version
Eshop server - Eureka service registry
Eshop Gateway - Zuul gateway
Eshop auth -- authorization service
Eshop member - Member Service
Eshop email - mail service (not used yet)
Eshop common -- general class
How to build a basic Spring Cloud microservice will not be repeated here. No, you can take a look at the author's blog about Spring Cloud series.
Here is an entry address: https://blog.csdn.net/wya1993/article/category/7701476
Authorization services
Firstly, build eshop auth service and introduce related dependencies
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>eshop-parent</artifactId> <groupId>com.curise.eshop</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>eshop-auth</artifactId> <packaging>war</packaging> <description>Authorization module</description> <dependencies> <dependency> <groupId>com.curise.eshop</groupId> <artifactId>eshop-common</artifactId> <version>1.0-SNAPSHOT</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Next, configure Mybatis, redis and eureka and paste the configuration file
server: port: 1203 spring: application: name: eshop-auth redis: database: 0 host: 192.168.0.117 port: 6379 password: jedis: pool: max-active: 8 max-idle: 8 min-idle: 0 datasource: driver-class-name: com.mysql.jdbc.Driver url: jdbc:mysql://localhost:3306/eshop_member?useUnicode=true&characterEncoding=utf-8&useSSL=false&allowMultiQueries=true username: root password: root druid: initialSize: 5 #Initialize connection size minIdle: 5 #Minimum number of connection pools maxActive: 20 #Maximum number of connection pools maxWait: 60000 #Maximum wait time to get a connection, in milliseconds timeBetweenEvictionRunsMillis: 60000 #Configure how often to detect idle connections that need to be closed. The unit is milliseconds minEvictableIdleTimeMillis: 300000 #Configure the minimum lifetime of a connection in the pool, in milliseconds validationQuery: SELECT 1 from DUAL #Test connection testWhileIdle: true #It is detected when applying for connection. It is recommended to configure it to true, which will not affect performance and ensure security testOnBorrow: false #The detection is performed when obtaining the connection. It is recommended to close it, which will affect the performance testOnReturn: false #Perform detection when returning the connection. It is recommended to close it, which will affect the performance poolPreparedStatements: false #Whether to enable PSCache. PSCache greatly improves the performance of databases supporting cursors. It is recommended to enable it in oracle and close it in mysql maxPoolPreparedStatementPerConnectionSize: 20 #It takes effect after poolPreparedStatements is enabled filters: stat,wall,log4j #Configure extensions. Common plug-ins are = > stat: monitoring statistics log4j: log wall: defending sql injection connectionProperties: 'druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000' #Open the mergeSql function through the connectProperties property; Slow SQL record eureka: instance: prefer-ip-address: true instance-id: ${spring.cloud.client.ip-address}:${server.port} client: service-url: defaultZone: http://localhost:1111/eureka/ mybatis: type-aliases-package: com.curise.eshop.common.entity configuration: map-underscore-to-camel-case: true #Open hump naming, l_ name -> lName jdbc-type-for-null: NULL lazy-loading-enabled: true aggressive-lazy-loading: true cache-enabled: true #Enable L2 cache call-setters-on-nulls: true #map empty columns do not show problems mapper-locations: - classpath:mybatis/*.xml
AuthApplication adds @ EnableDiscoveryClient and @ MapperScan annotations.
Next, configure the authentication server AuthorizationServerConfig, and add @ Configuration and @ EnableAuthorizationServer annotations. The ClientDetailsServiceConfigurer is configured in memory. Of course, it can also be read from the database, which will be improved gradually in the future.
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private AuthenticationManager authenticationManager; @Autowired private DataSource dataSource; @Autowired private RedisConnectionFactory redisConnectionFactory; @Autowired private MyUserDetailService userDetailService; @Bean public TokenStore tokenStore() { return new RedisTokenStore(redisConnectionFactory); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security .allowFormAuthenticationForClients() .tokenKeyAccess("permitAll()") .checkTokenAccess("isAuthenticated()"); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // clients.withClientDetails(clientDetails()); clients.inMemory() .withClient("android") .scopes("read") .secret("android") .authorizedGrantTypes("password", "authorization_code", "refresh_token") .and() .withClient("webapp") .scopes("read") .authorizedGrantTypes("implicit") .and() .withClient("browser") .authorizedGrantTypes("refresh_token", "password") .scopes("read"); } @Bean public ClientDetailsService clientDetails() { return new JdbcClientDetailsService(dataSource); } @Bean public WebResponseExceptionTranslator webResponseExceptionTranslator(){ return new MssWebResponseExceptionTranslator(); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore()) .userDetailsService(userDetailService) .authenticationManager(authenticationManager); endpoints.tokenServices(defaultTokenServices()); //Authentication exception translation // endpoints.exceptionTranslator(webResponseExceptionTranslator()); } /** * <p>Note that when customizing TokenServices, you need to set @ Primary, otherwise an error will be reported</p> * @return */ @Primary @Bean public DefaultTokenServices defaultTokenServices(){ DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(tokenStore()); tokenServices.setSupportRefreshToken(true); //tokenServices.setClientDetailsService(clientDetails()); // The validity period of the token is set by user. The default is 12 hours tokenServices.setAccessTokenValiditySeconds(60*60*12); // refresh_ The default token is 30 days tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7); return tokenServices; } }
In the above configuration, the authenticated token is stored in redis. If you use spring 5 For versions above 0, the following exceptions will be reported when using the default RedisTokenStore authentication:
nested exception is java.lang.NoSuchMethodError: org.springframework.data.redis.connection.RedisConnection.set([B[B)V
The reason is that in spring data redis version 2.0, set(String,String) is discarded and redisconnection is used stringCommands(). Set (...), I customize a RedisTokenStore. The code is the same as RedisTokenStore, except that all conn.set(...) are replaced by Conn stringCommands(). Set (...), the method is feasible after test.
public class RedisTokenStore implements TokenStore { private static final String ACCESS = "access:"; private static final String AUTH_TO_ACCESS = "auth_to_access:"; private static final String AUTH = "auth:"; private static final String REFRESH_AUTH = "refresh_auth:"; private static final String ACCESS_TO_REFRESH = "access_to_refresh:"; private static final String REFRESH = "refresh:"; private static final String REFRESH_TO_ACCESS = "refresh_to_access:"; private static final String CLIENT_ID_TO_ACCESS = "client_id_to_access:"; private static final String UNAME_TO_ACCESS = "uname_to_access:"; private final RedisConnectionFactory connectionFactory; private AuthenticationKeyGenerator authenticationKeyGenerator = new DefaultAuthenticationKeyGenerator(); private RedisTokenStoreSerializationStrategy serializationStrategy = new JdkSerializationStrategy(); private String prefix = ""; public RedisTokenStore(RedisConnectionFactory connectionFactory) { this.connectionFactory = connectionFactory; } public void setAuthenticationKeyGenerator(AuthenticationKeyGenerator authenticationKeyGenerator) { this.authenticationKeyGenerator = authenticationKeyGenerator; } public void setSerializationStrategy(RedisTokenStoreSerializationStrategy serializationStrategy) { this.serializationStrategy = serializationStrategy; } public void setPrefix(String prefix) { this.prefix = prefix; } private RedisConnection getConnection() { return this.connectionFactory.getConnection(); } private byte[] serialize(Object object) { return this.serializationStrategy.serialize(object); } private byte[] serializeKey(String object) { return this.serialize(this.prefix + object); } private OAuth2AccessToken deserializeAccessToken(byte[] bytes) { return (OAuth2AccessToken)this.serializationStrategy.deserialize(bytes, OAuth2AccessToken.class); } private OAuth2Authentication deserializeAuthentication(byte[] bytes) { return (OAuth2Authentication)this.serializationStrategy.deserialize(bytes, OAuth2Authentication.class); } private OAuth2RefreshToken deserializeRefreshToken(byte[] bytes) { return (OAuth2RefreshToken)this.serializationStrategy.deserialize(bytes, OAuth2RefreshToken.class); } private byte[] serialize(String string) { return this.serializationStrategy.serialize(string); } private String deserializeString(byte[] bytes) { return this.serializationStrategy.deserializeString(bytes); } @Override public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) { String key = this.authenticationKeyGenerator.extractKey(authentication); byte[] serializedKey = this.serializeKey(AUTH_TO_ACCESS + key); byte[] bytes = null; RedisConnection conn = this.getConnection(); try { bytes = conn.get(serializedKey); } finally { conn.close(); } OAuth2AccessToken accessToken = this.deserializeAccessToken(bytes); if (accessToken != null) { OAuth2Authentication storedAuthentication = this.readAuthentication(accessToken.getValue()); if (storedAuthentication == null || !key.equals(this.authenticationKeyGenerator.extractKey(storedAuthentication))) { this.storeAccessToken(accessToken, authentication); } } return accessToken; } @Override public OAuth2Authentication readAuthentication(OAuth2AccessToken token) { return this.readAuthentication(token.getValue()); } @Override public OAuth2Authentication readAuthentication(String token) { byte[] bytes = null; RedisConnection conn = this.getConnection(); try { bytes = conn.get(this.serializeKey("auth:" + token)); } finally { conn.close(); } OAuth2Authentication auth = this.deserializeAuthentication(bytes); return auth; } @Override public OAuth2Authentication readAuthenticationForRefreshToken(OAuth2RefreshToken token) { return this.readAuthenticationForRefreshToken(token.getValue()); } public OAuth2Authentication readAuthenticationForRefreshToken(String token) { RedisConnection conn = getConnection(); try { byte[] bytes = conn.get(serializeKey(REFRESH_AUTH + token)); OAuth2Authentication auth = deserializeAuthentication(bytes); return auth; } finally { conn.close(); } } @Override public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { byte[] serializedAccessToken = serialize(token); byte[] serializedAuth = serialize(authentication); byte[] accessKey = serializeKey(ACCESS + token.getValue()); byte[] authKey = serializeKey(AUTH + token.getValue()); byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + authenticationKeyGenerator.extractKey(authentication)); byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication)); byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId()); RedisConnection conn = getConnection(); try { conn.openPipeline(); conn.stringCommands().set(accessKey, serializedAccessToken); conn.stringCommands().set(authKey, serializedAuth); conn.stringCommands().set(authToAccessKey, serializedAccessToken); if (!authentication.isClientOnly()) { conn.rPush(approvalKey, serializedAccessToken); } conn.rPush(clientId, serializedAccessToken); if (token.getExpiration() != null) { int seconds = token.getExpiresIn(); conn.expire(accessKey, seconds); conn.expire(authKey, seconds); conn.expire(authToAccessKey, seconds); conn.expire(clientId, seconds); conn.expire(approvalKey, seconds); } OAuth2RefreshToken refreshToken = token.getRefreshToken(); if (refreshToken != null && refreshToken.getValue() != null) { byte[] refresh = serialize(token.getRefreshToken().getValue()); byte[] auth = serialize(token.getValue()); byte[] refreshToAccessKey = serializeKey(REFRESH_TO_ACCESS + token.getRefreshToken().getValue()); conn.stringCommands().set(refreshToAccessKey, auth); byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + token.getValue()); conn.stringCommands().set(accessToRefreshKey, refresh); if (refreshToken instanceof ExpiringOAuth2RefreshToken) { ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken; Date expiration = expiringRefreshToken.getExpiration(); if (expiration != null) { int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L) .intValue(); conn.expire(refreshToAccessKey, seconds); conn.expire(accessToRefreshKey, seconds); } } } conn.closePipeline(); } finally { conn.close(); } } private static String getApprovalKey(OAuth2Authentication authentication) { String userName = authentication.getUserAuthentication() == null ? "": authentication.getUserAuthentication().getName(); return getApprovalKey(authentication.getOAuth2Request().getClientId(), userName); } private static String getApprovalKey(String clientId, String userName) { return clientId + (userName == null ? "" : ":" + userName); } @Override public void removeAccessToken(OAuth2AccessToken accessToken) { this.removeAccessToken(accessToken.getValue()); } @Override public OAuth2AccessToken readAccessToken(String tokenValue) { byte[] key = serializeKey(ACCESS + tokenValue); byte[] bytes = null; RedisConnection conn = getConnection(); try { bytes = conn.get(key); } finally { conn.close(); } OAuth2AccessToken accessToken = deserializeAccessToken(bytes); return accessToken; } public void removeAccessToken(String tokenValue) { byte[] accessKey = serializeKey(ACCESS + tokenValue); byte[] authKey = serializeKey(AUTH + tokenValue); byte[] accessToRefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue); RedisConnection conn = getConnection(); try { conn.openPipeline(); conn.get(accessKey); conn.get(authKey); conn.del(accessKey); conn.del(accessToRefreshKey); // Don't remove the refresh token - it's up to the caller to do that conn.del(authKey); List<Object> results = conn.closePipeline(); byte[] access = (byte[]) results.get(0); byte[] auth = (byte[]) results.get(1); OAuth2Authentication authentication = deserializeAuthentication(auth); if (authentication != null) { String key = authenticationKeyGenerator.extractKey(authentication); byte[] authToAccessKey = serializeKey(AUTH_TO_ACCESS + key); byte[] unameKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(authentication)); byte[] clientId = serializeKey(CLIENT_ID_TO_ACCESS + authentication.getOAuth2Request().getClientId()); conn.openPipeline(); conn.del(authToAccessKey); conn.lRem(unameKey, 1, access); conn.lRem(clientId, 1, access); conn.del(serialize(ACCESS + key)); conn.closePipeline(); } } finally { conn.close(); } } @Override public void storeRefreshToken(OAuth2RefreshToken refreshToken, OAuth2Authentication authentication) { byte[] refreshKey = serializeKey(REFRESH + refreshToken.getValue()); byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + refreshToken.getValue()); byte[] serializedRefreshToken = serialize(refreshToken); RedisConnection conn = getConnection(); try { conn.openPipeline(); conn.stringCommands().set(refreshKey, serializedRefreshToken); conn.stringCommands().set(refreshAuthKey, serialize(authentication)); if (refreshToken instanceof ExpiringOAuth2RefreshToken) { ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken) refreshToken; Date expiration = expiringRefreshToken.getExpiration(); if (expiration != null) { int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L) .intValue(); conn.expire(refreshKey, seconds); conn.expire(refreshAuthKey, seconds); } } conn.closePipeline(); } finally { conn.close(); } } @Override public OAuth2RefreshToken readRefreshToken(String tokenValue) { byte[] key = serializeKey(REFRESH + tokenValue); byte[] bytes = null; RedisConnection conn = getConnection(); try { bytes = conn.get(key); } finally { conn.close(); } OAuth2RefreshToken refreshToken = deserializeRefreshToken(bytes); return refreshToken; } @Override public void removeRefreshToken(OAuth2RefreshToken refreshToken) { this.removeRefreshToken(refreshToken.getValue()); } public void removeRefreshToken(String tokenValue) { byte[] refreshKey = serializeKey(REFRESH + tokenValue); byte[] refreshAuthKey = serializeKey(REFRESH_AUTH + tokenValue); byte[] refresh2AccessKey = serializeKey(REFRESH_TO_ACCESS + tokenValue); byte[] access2RefreshKey = serializeKey(ACCESS_TO_REFRESH + tokenValue); RedisConnection conn = getConnection(); try { conn.openPipeline(); conn.del(refreshKey); conn.del(refreshAuthKey); conn.del(refresh2AccessKey); conn.del(access2RefreshKey); conn.closePipeline(); } finally { conn.close(); } } @Override public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) { this.removeAccessTokenUsingRefreshToken(refreshToken.getValue()); } private void removeAccessTokenUsingRefreshToken(String refreshToken) { byte[] key = serializeKey(REFRESH_TO_ACCESS + refreshToken); List<Object> results = null; RedisConnection conn = getConnection(); try { conn.openPipeline(); conn.get(key); conn.del(key); results = conn.closePipeline(); } finally { conn.close(); } if (results == null) { return; } byte[] bytes = (byte[]) results.get(0); String accessToken = deserializeString(bytes); if (accessToken != null) { removeAccessToken(accessToken); } } @Override public Collection<OAuth2AccessToken> findTokensByClientIdAndUserName(String clientId, String userName) { byte[] approvalKey = serializeKey(UNAME_TO_ACCESS + getApprovalKey(clientId, userName)); List<byte[]> byteList = null; RedisConnection conn = getConnection(); try { byteList = conn.lRange(approvalKey, 0, -1); } finally { conn.close(); } if (byteList == null || byteList.size() == 0) { return Collections.<OAuth2AccessToken> emptySet(); } List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(byteList.size()); for (byte[] bytes : byteList) { OAuth2AccessToken accessToken = deserializeAccessToken(bytes); accessTokens.add(accessToken); } return Collections.<OAuth2AccessToken> unmodifiableCollection(accessTokens); } @Override public Collection<OAuth2AccessToken> findTokensByClientId(String clientId) { byte[] key = serializeKey(CLIENT_ID_TO_ACCESS + clientId); List<byte[]> byteList = null; RedisConnection conn = getConnection(); try { byteList = conn.lRange(key, 0, -1); } finally { conn.close(); } if (byteList == null || byteList.size() == 0) { return Collections.<OAuth2AccessToken> emptySet(); } List<OAuth2AccessToken> accessTokens = new ArrayList<OAuth2AccessToken>(byteList.size()); for (byte[] bytes : byteList) { OAuth2AccessToken accessToken = deserializeAccessToken(bytes); accessTokens.add(accessToken); } return Collections.<OAuth2AccessToken> unmodifiableCollection(accessTokens); } }
Configure resource server
@Configuration @EnableResourceServer @Order(3) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .csrf().disable() .exceptionHandling() .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) .and() .requestMatchers().antMatchers("/api/**") .and() .authorizeRequests() .antMatchers("/api/**").authenticated() .and() .httpBasic(); } }
Configure Spring Security
@Configuration @EnableWebSecurity @Order(2) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailService userDetailService; @Bean public PasswordEncoder passwordEncoder() { //return new BCryptPasswordEncoder(); return new NoEncryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { http.requestMatchers().antMatchers("/oauth/**") .and() .authorizeRequests() .antMatchers("/oauth/**").authenticated() .and() .csrf().disable(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailService).passwordEncoder(passwordEncoder()); } /** * Do not define grant without password_ type * * @return * @throws Exception */ @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
You can see that ResourceServerConfig has lower priority than SecurityConfig.
Relationship between the two:
-
ResourceServerConfig is used to protect oauth related endpoints and is mainly used for user login (form login,Basic auth)
-
SecurityConfig is used to protect the resources to be opened by oauth. At the same time, it is mainly used for client side and token authentication (Bearer auth)
Therefore, we make SecurityConfig take precedence over ResourceServerConfig, and SecurityConfig does not intercept oauth the resources to be opened. We configure the resources that need token authentication in ResourceServerConfig, that is, the interfaces we provide externally. Therefore, there is a requirement for the interface definitions of all microservices, that is, they all start with / api.
If you don't configure it like this, after you get access_ When token requests each interface, it will report invalid_token prompt.
In addition, because we customize the authentication logic, we need to override UserDetailService
@Service("userDetailService") public class MyUserDetailService implements UserDetailsService { @Autowired private MemberDao memberDao; @Override public UserDetails loadUserByUsername(String memberName) throws UsernameNotFoundException { Member member = memberDao.findByMemberName(memberName); if (member == null) { throw new UsernameNotFoundException(memberName); } Set<GrantedAuthority> grantedAuthorities = new HashSet<>(); // Availability: true: available false: not available boolean enabled = true; // Expiration: true: no expiration; false: expiration boolean accountNonExpired = true; // Validity: true: voucher valid false: voucher invalid boolean credentialsNonExpired = true; // Lockability: true: unlocked false: locked boolean accountNonLocked = true; for (Role role : member.getRoles()) { //ROLE must be ROLE_ At the beginning, it can be set in the database GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(role.getRoleName()); grantedAuthorities.add(grantedAuthority); //Get permission for (Permission permission : role.getPermissions()) { GrantedAuthority authority = new SimpleGrantedAuthority(permission.getUri()); grantedAuthorities.add(authority); } } User user = new User(member.getMemberName(), member.getPassword(), enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuthorities); return user; } }
In order to facilitate password verification, I used the non encrypted method and rewritten PasswordEncoder. BCryptPasswordEncoder is recommended for actual development.
public class NoEncryptPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence charSequence) { return (String) charSequence; } @Override public boolean matches(CharSequence charSequence, String s) { return s.equals((String) charSequence); } }
In addition, OAuth's password mode requires authentication manager support
@Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
Define a Controller and provide two interfaces, / api/member is used to obtain the current user information, and / api/exit is used to log off the current user
@RestController @RequestMapping("/api") public class MemberController { @Autowired private MyUserDetailService userDetailService; @Autowired private ConsumerTokenServices consumerTokenServices; @GetMapping("/member") public Principal user(Principal member) { return member; } @DeleteMapping(value = "/exit") public Result revokeToken(String access_token) { Result result = new Result(); if (consumerTokenServices.revokeToken(access_token)) { result.setCode(ResultCode.SUCCESS.getCode()); result.setMessage("Logout succeeded"); } else { result.setCode(ResultCode.FAILED.getCode()); result.setMessage("Logoff failed"); } return result; } }
Member service configuration
Introduce dependency
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>eshop-parent</artifactId> <groupId>com.curise.eshop</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <artifactId>eshop-member</artifactId> <packaging>war</packaging> <description>Membership module</description> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Configure resource server
@Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http .csrf().disable() .exceptionHandling() .authenticationEntryPoint((request, response, authException) -> response.sendError(HttpServletResponse.SC_UNAUTHORIZED)) .and() .requestMatchers().antMatchers("/api/**") .and() .authorizeRequests() .antMatchers("/api/**").authenticated() .and() .httpBasic(); } }
Profile configuration
spring: application: name: eshop-member server: port: 1201 eureka: instance: prefer-ip-address: true instance-id: ${spring.cloud.client.ip-address}:${server.port} client: service-url: defaultZone: http://localhost:1111/eureka/ security: oauth2: resource: id: eshop-member user-info-uri: http://localhost:1202/auth/api/member prefer-token-info: false
MemberApplication main class configuration
@SpringBootApplication @EnableDiscoveryClient @EnableGlobalMethodSecurity(prePostEnabled = true) public class MemberApplication { public static void main(String[] args) { SpringApplication.run(MemberApplication.class,args); } }
Provide external interface
@RestController @RequestMapping("/api") public class MemberController { @GetMapping("hello") @PreAuthorize("hasAnyAuthority('hello')") public String hello(){ return "hello"; } @GetMapping("current") public Principal user(Principal principal) { return principal; } @GetMapping("query") @PreAuthorize("hasAnyAuthority('query')") public String query() { return "have query jurisdiction"; } }
configure gateway
Introduce dependency
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>eshop-parent</artifactId> <groupId>com.curise.eshop</groupId> <version>1.0-SNAPSHOT</version> </parent> <modelVersion>4.0.0</modelVersion> <packaging>jar</packaging> <artifactId>eshop-gateway</artifactId> <description>gateway</description> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-zuul</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-oauth2</artifactId> </dependency> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-actuator</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
Configuration file
server: port: 1202 spring: application: name: eshop-gateway #--------------------eureka--------------------- eureka: instance: prefer-ip-address: true instance-id: ${spring.cloud.client.ip-address}:${server.port} client: service-url: defaultZone: http://localhost:1111/eureka/ #--------------------Zuul----------------------- zuul: routes: member: path: /member/** serviceId: eshop-member sensitiveHeaders: "*" auth: path: /auth/** serviceId: eshop-auth sensitiveHeaders: "*" retryable: false ignored-services: "*" ribbon: eager-load: enabled: true host: connect-timeout-millis: 3000 socket-timeout-millis: 3000 add-proxy-headers: true #---------------------OAuth2--------------------- security: oauth2: client: access-token-uri: http://localhost:${server.port}/auth/oauth/token user-authorization-uri: http://localhost:${server.port}/auth/oauth/authorize client-id: web resource: user-info-uri: http://localhost:${server.port}/auth/api/member prefer-token-info: false #----------------------Timeout configuration------------------- ribbon: ReadTimeout: 3000 ConnectTimeout: 3000 MaxAutoRetries: 1 MaxAutoRetriesNextServer: 2 eureka: enabled: true hystrix: command: default: execution: timeout: enabled: true isolation: thread: timeoutInMilliseconds: 3500
ZuulApplication main class
@SpringBootApplication @EnableDiscoveryClient @EnableZuulProxy @EnableOAuth2Sso public class ZuulApplication { public static void main(String[] args) { SpringApplication.run(ZuulApplication.class, args); } }
Spring Security configuration
@Configuration @EnableWebSecurity @Order(99) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); } }
Next, start eshop server, eshop member, eshop auth and eshop gateway respectively.
First send a request to test the effect of unauthentication
Obtain certification
Using access_ The token requests the user information interface under the auth service
Using access_ The user information interface under the token request member service
query interface for requesting member Service
The hello interface for requesting the member service does not give the user hello permission in the database
Refresh token
cancellation
The follow-up will be improved slowly. Please look forward to it!!
sql about the code and data table has been uploaded to GitHub. Address: https://github.com/WYA1993/springcloud_oauth2.0 .
Pay attention to replacing the database and redis with their own addresses
In a unified reply, many people reported that they returned 401 when obtaining authentication, as follows:
{
"timestamp": "2019-08-13T03:25:27.161+0000",
"status": 401,
"error": "Unauthorized",
"message": "Unauthorized",
"path": "/oauth/token"
}
The reason is that Basic Auth authentication was not added when the request was initiated, as shown in the following figure:
, after adding Basic Auth authentication, an authentication header will be added in the headers
The information of adding Basic Auth authentication is reflected in the code:
The client information and token information are obtained from the MySQL database
Now the client information is stored in memory, which is definitely not allowed in the production environment. To support the dynamic addition or deletion of the client, I choose to save the client information in MySQL.
First, create a data table. The structure of the data table has been officially given. The address is
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
Secondly, you need to modify the sql script. Change the length of the primary key to 128 and the LONGVARBINARY type to blob. The adjusted sql script:
create table oauth_client_details ( client_id VARCHAR(128) PRIMARY KEY, resource_ids VARCHAR(256), client_secret VARCHAR(256), scope VARCHAR(256), authorized_grant_types VARCHAR(256), web_server_redirect_uri VARCHAR(256), authorities VARCHAR(256), access_token_validity INTEGER, refresh_token_validity INTEGER, additional_information VARCHAR(4096), autoapprove VARCHAR(256) ); create table oauth_client_token ( token_id VARCHAR(256), token BLOB, authentication_id VARCHAR(128) PRIMARY KEY, user_name VARCHAR(256), client_id VARCHAR(256) ); create table oauth_access_token ( token_id VARCHAR(256), token BLOB, authentication_id VARCHAR(128) PRIMARY KEY, user_name VARCHAR(256), client_id VARCHAR(256), authentication BLOB, refresh_token VARCHAR(256) ); create table oauth_refresh_token ( token_id VARCHAR(256), token BLOB, authentication BLOB ); create table oauth_code ( code VARCHAR(256), authentication BLOB ); create table oauth_approvals ( userId VARCHAR(256), clientId VARCHAR(256), scope VARCHAR(256), status VARCHAR(10), expiresAt TIMESTAMP, lastModifiedAt TIMESTAMP ); -- customized oauth_client_details table create table ClientDetails ( appId VARCHAR(128) PRIMARY KEY, resourceIds VARCHAR(256), appSecret VARCHAR(256), scope VARCHAR(256), grantTypes VARCHAR(256), redirectUrl VARCHAR(256), authorities VARCHAR(256), access_token_validity INTEGER, refresh_token_validity INTEGER, additionalInformation VARCHAR(4096), autoApproveScopes VARCHAR(256) );
The adjusted sql steps are also put into GitHub, and you can download them yourself if necessary
Then in eshop_ Create a data table in the member database and add the client information to OAuth_ client_ In the details table
If your password is not clear text, remember client_secret needs to be stored after encryption.
Then modify the code to configure the client information to be read from the database
Next, start the service test.
Get authorization
Get user information
Refresh token
Open the data table and find that the token information is not saved in the table. Because the token store uses redis mode, we can read it from the database instead. Modify configuration
Restart the service and test again
Check the data table and find that the token data has been saved in the table.
Source address: https://github.com/WYA1993/springcloud_oauth2.0 .
From: CSDN, author: myCat
Link: https://blog.csdn.net/WYA1993/article/details/85050120