個人 OAuth2 全部文章
- Spring Security 與 OAuth2(介紹):http://www.lxweimin.com/p/68f22f9a00ee
- Spring Security 與 OAuth2(授權服務器):http://www.lxweimin.com/p/227f7e7503cb
- Spring Security 與 OAuth2(資源服務器):http://www.lxweimin.com/p/6dd03375224d
- Spring Security 與 OAuth2(客戶端):http://www.lxweimin.com/p/03e515c2b43f
- Spring Security 與 OAuth2(相關類參考):http://www.lxweimin.com/p/c2395772bc86
- Spring Security 與 OAuth2(完整案例):http://www.lxweimin.com/p/d80061e6d900
案例簡述
簡述:
- 允許內(nèi)存、數(shù)據(jù)庫、JWT等方式存儲令牌
- 允許 JWT 方式驗證令牌
- 允許從內(nèi)存、數(shù)據(jù)庫中讀取客戶端詳情
- 封裝配置類,簡化配置,通過注解方式定制使用何種令牌存儲方式、客戶端詳情獲取方式,可使用 JWT 方式存儲令牌,從數(shù)據(jù)庫中獲取客戶端詳情
- 提供完整單元測試
- 較為詳細的代碼注釋
-
允許從授權服務器(適用于 JDBC、內(nèi)存存儲令牌)驗證令牌該代碼尚未完善,僅供參考 - 數(shù)據(jù)庫 Schema : https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
- Demo Git 地址:https://gitee.com/LinYuanTongXue/OAuth2-Demo
Demo 流程:
- 使用 OAuth2 密碼授權方式提供令牌
- 資源服務器1(也為客戶端)提供登錄接口,資源所有者(用戶)通過將個人賬號密碼提供給 資源服務器1,資源服務器1 通過該信息向授權服務器獲取令牌
- 資源服務器1(也為客戶端)通過令牌(其中包含了客戶端、用戶等信息)訪問自身受保護的資源(需要權限才能查看的資源)
- 資源服務器2(也可資源服務器)不包含登錄接口,但其提供了某些受保護的資源(需要資源服務器1帶著訪問令牌才能訪問)
- 資源服務器1(也為客戶端)通過令牌向 資源服務器2(資源服務器) 請求其受保護的資源
使用
- 授權服務器通過繼承 AuthServerConfig 抽象類來配置授權服務器
- 資源服務器、客戶端通過繼承 ResServerConfig 抽象類來配置資源服務器
- Web 權限配置可繼承 AbstractSecurityConfig 抽象類來簡化配置
- 授權服務器通過在啟動類添加以下注解來設置令牌存儲、客戶端信息獲取方式
- @EnableAuthJWTTokenStore:使用 JWT 存儲令牌
- @EnableDBClientDetailsService:通過數(shù)據(jù)庫獲取客戶端詳情
- @EnableDBTokenStore:通過數(shù)據(jù)庫存儲令牌
- 資源服務器通過在啟動類添加以下注解來設置令牌解析方式
- @EnableResJWTTokenStore:使用 JWT 解析令牌
-
@EnableRemoteTokenService:通過授權服務器驗證令牌該代碼尚未完善,僅供參考
項目結構
- 下圖是 Demo 項目結構,使用了 Maven 之間的繼承關系,并添加了熱部署,不了解的可以查看下 Git 上的 Demo 源碼
- oauth2-config:該包中定義了一些通用的類,例如授權服務器、資源服務器配置類,服務繼承該類來簡化配置
- authentication-server:授權服務器
- resource1-server:資源服務器1(也為客戶端)
- resource2-server:資源服務器2(也為資源服務器)
bd1je.png
代碼
oauth2-config(通用配置類庫)
權限枚舉常量
/**
* @author: 林塬
* @date: 2018/1/20
* @description: 權限常量
*/
public enum AuthoritiesEnum {
ADMIN("ROLE_ADMIN"),
USER("ROLE_USER"),
ANONYMOUS("ROLE_ANONYMOUS");
private String role;
AuthoritiesEnum(String role) {
this.role = role;
}
public String getRole() {
return role;
}
}
授權服務器 JWT 方式存儲令牌
/**
* @author: 林塬
* @date: 2018/1/20
* @description: 授權服務器 TokenStore 配置類,使用 JWT RSA 非對稱加密
*/
public class AuthJWTTokenStore {
@Bean("keyProp")
public KeyProperties keyProperties(){
return new KeyProperties();
}
@Resource(name = "keyProp")
private KeyProperties keyProperties;
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
KeyPair keyPair = new KeyStoreKeyFactory
(keyProperties.getKeyStore().getLocation(), keyProperties.getKeyStore().getSecret().toCharArray())
.getKeyPair(keyProperties.getKeyStore().getAlias());
converter.setKeyPair(keyPair);
return converter;
}
}
資源服務器 JWT 方式解析令牌
/**
* @author: 林塬
* @date: 2018/1/20
* @description: 資源服務器 TokenStore 配置類,使用 JWT RSA 非對稱加密
*/
public class ResJWTTokenStore {
private static final String PUBLIC_KEY = "pubkey.txt";
@Autowired
private ResourceServerProperties resourceServerProperties;
@Bean
public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
return new JwtTokenStore(jwtAccessTokenConverter);
}
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setVerifierKey(getPubKey());
return converter;
}
/**
* 獲取非對稱加密公鑰 Key
* @return 公鑰 Key
*/
private String getPubKey() {
Resource resource = new ClassPathResource(ResJWTTokenStore.PUBLIC_KEY);
try (BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
return br.lines().collect(Collectors.joining("\n"));
} catch (IOException ioe) {
return getKeyFromAuthorizationServer();
}
}
/**
* 通過訪問授權服務器獲取非對稱加密公鑰 Key
* @return 公鑰 Key
*/
private String getKeyFromAuthorizationServer() {
ObjectMapper objectMapper = new ObjectMapper();
String pubKey = new RestTemplate().getForObject(resourceServerProperties.getJwt().getKeyUri(), String.class);
try {
Map map = objectMapper.readValue(pubKey, Map.class);
return map.get("value").toString();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}
數(shù)據(jù)庫方式存儲令牌
/**
* @author: 林塬
* @date: 2018/1/22
* @description: 使用數(shù)據(jù)庫存取令牌
*/
public class DBTokenStore {
@Autowired
private DataSource dataSource;
@Bean
public TokenStore tokenStore(){
return new JdbcTokenStore(dataSource);
}
}
數(shù)據(jù)庫方式加載客戶端詳情
/**
* @author: 林塬
* @date: 2018/1/22
* @description: 通過數(shù)據(jù)庫加載客戶端詳情
*/
public class DBClientDetailsService {
@Autowired
private DataSource dataSource;
@Bean
public ClientDetailsService clientDetailsService(){
return new JdbcClientDetailsService(dataSource);
}
}
授權服務器解析令牌
/**
* @author: 林塬
* @date: 2018/1/22
* @description: 通過訪問遠程授權服務器 check_token 端點驗證令牌
*/
public class RemoteTokenService {
@Autowired
private OAuth2ClientProperties oAuth2ClientProperties;
@Resource(name = "authServerProp")
private AuthorizationServerProperties authorizationServerProperties;
@Bean(name = "authServerProp")
public AuthorizationServerProperties authorizationServerProperties(){
return new AuthorizationServerProperties();
}
@Bean
public ResourceServerTokenServices tokenServices() {
RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
remoteTokenServices.setCheckTokenEndpointUrl(authorizationServerProperties.getCheckTokenAccess());
remoteTokenServices.setClientId(oAuth2ClientProperties.getClientId());
remoteTokenServices.setClientSecret(oAuth2ClientProperties.getClientSecret());
remoteTokenServices.setAccessTokenConverter(accessTokenConverter());
return remoteTokenServices;
}
@Bean
public AccessTokenConverter accessTokenConverter() {
return new DefaultAccessTokenConverter();
}
}
WebSecurity 權限類
/**
* @author: 林塬
* @date: 2018/1/20
* @description: Web 權限配置類
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public abstract class AbstractSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private AuthenticationManagerBuilder authenticationManagerBuilder;
@Autowired
private UserDetailsService userDetailsService;
@PostConstruct
public void init() {
try {
authenticationManagerBuilder
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder());
} catch (Exception e) {
throw new BeanInitializationException("Security configuration failed", e);
}
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.and()
.csrf()
.disable()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/swagger-ui/index.html").hasAuthority(AuthoritiesEnum.ADMIN.getRole());
}
授權服務器配置類
/**
* @author: 林塬
* @date: 2018/1/20
* @description: OAuth2 授權服務器配置類
*/
@EnableAuthorizationServer
public abstract class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private ApplicationContext applicationContext;
@Autowired
private TokenStore tokenStore;
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired(required = false)
private JdbcClientDetailsService jdbcClientDetailsService;
//令牌失效時間
public int accessTokenValiditySeconds;
//刷新令牌失效時間
public int refreshTokenValiditySeconds;
//是否可以重用刷新令牌
public boolean isReuseRefreshToken;
//是否支持刷新令牌
public boolean isSupportRefreshToken;
public AuthServerConfig(int accessTokenValiditySeconds, int refreshTokenValiditySeconds, boolean isReuseRefreshToken, boolean isSupportRefreshToken) {
this.accessTokenValiditySeconds = accessTokenValiditySeconds;
this.refreshTokenValiditySeconds = refreshTokenValiditySeconds;
this.isReuseRefreshToken = isReuseRefreshToken;
this.isSupportRefreshToken = isSupportRefreshToken;
}
/**
* 配置授權服務器端點,如令牌存儲,令牌自定義,用戶批準和授權類型,不包括端點安全配置
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
Collection<TokenEnhancer> tokenEnhancers = applicationContext.getBeansOfType(TokenEnhancer.class).values();
TokenEnhancerChain tokenEnhancerChain=new TokenEnhancerChain();
tokenEnhancerChain.setTokenEnhancers(new ArrayList<>(tokenEnhancers));
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setReuseRefreshToken(isReuseRefreshToken);
defaultTokenServices.setSupportRefreshToken(isSupportRefreshToken);
defaultTokenServices.setTokenStore(tokenStore);
defaultTokenServices.setAccessTokenValiditySeconds(accessTokenValiditySeconds);
defaultTokenServices.setRefreshTokenValiditySeconds(refreshTokenValiditySeconds);
defaultTokenServices.setTokenEnhancer(tokenEnhancerChain);
//若通過 JDBC 存儲令牌
if (Objects.nonNull(jdbcClientDetailsService)){
defaultTokenServices.setClientDetailsService(jdbcClientDetailsService);
}
endpoints
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
.tokenServices(defaultTokenServices);
}
/**
* 配置授權服務器端點的安全
* @param oauthServer
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
}
資源服務器配置類
/**
* @author: 林塬
* @date: 2018/1/20
* @description: OAuth2 資源服務器配置類
*/
@EnableResourceServer
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public abstract class ResServerConfig extends ResourceServerConfigurerAdapter {
@Autowired(required = false)
private RemoteTokenServices remoteTokenServices;
@Autowired
private OAuth2ClientProperties oAuth2ClientProperties;
@Bean
@Qualifier("authorizationHeaderRequestMatcher")
public RequestMatcher authorizationHeaderRequestMatcher() {
return new RequestHeaderRequestMatcher("Authorization");
}
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
.exceptionHandling()
.and()
.headers()
.frameOptions()
.disable()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.requestMatcher(authorizationHeaderRequestMatcher());
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
super.configure(resources);
resources.resourceId(oAuth2ClientProperties.getClientId());
if (Objects.nonNull(remoteTokenServices)) {
resources.tokenServices(remoteTokenServices);
}
}
}
注解
/**
* @author: 林塬
* @date: 2018/1/22
* @description: 在啟動類上添加該注解來----開啟 JWT 令牌存儲(授權服務器-非對稱加密)
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(AuthJWTTokenStore.class)
public @interface EnableAuthJWTTokenStore {
}
/**
* @author: 林塬
* @date: 2018/1/22
* @description: 在啟動類上添加該注解來----開啟從數(shù)據(jù)庫加載客戶端詳情
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(DBClientDetailsService.class)
public @interface EnableDBClientDetailsService {
}
/**
* @author: 林塬
* @date: 2018/1/22
* @description: 在啟動類上添加該注解來----開啟通過數(shù)據(jù)庫存儲令牌
* 數(shù)據(jù)庫 schema :https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(DBTokenStore.class)
public @interface EnableDBTokenStore {
}
/**
* @author: 林塬
* @date: 2018/1/22
* @description: 在啟動類上添加該注解來----開啟通過授權服務器驗證訪問令牌(適用于 JDBC、內(nèi)存存儲令牌)
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(RemoteTokenService.class)
public @interface EnableRemoteTokenService {
}
/**
* @author: 林塬
* @date: 2018/1/22
* @description: 在啟動類上添加該注解來----開啟 JWT 令牌存儲(資源服務器-非對稱加密)
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ResJWTTokenStore.class)
public @interface EnableResJWTTokenStore {
}
authentication-server(授權服務器)
授權服務器配置
/**
* @author: 林塬
* @date: 2018/1/10
* @description: OAuth2 授權服務器配置
*/
@Configuration
public class AuthorizationServerConfig extends AuthServerConfig {
/**
* 調(diào)用父類構造函數(shù),設置令牌失效日期等信息
*/
public AuthorizationServerConfig() {
super((int)TimeUnit.DAYS.toSeconds(1), 0, false, false);
}
/**
* 配置客戶端詳情
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure(clients);
clients.inMemory() // 使用內(nèi)存存儲客戶端信息
.withClient("resource1") // client_id
.secret("secret") // client_secret
.authorizedGrantTypes("authorization_code","password") // 該client允許的授權類型
.scopes("read") // 允許的授權范圍
.autoApprove(true); //登錄后繞過批準詢問(/oauth/confirm_access)
}
}
WebSecurity 權限配置
/**
* @author: 林塬
* @date: 2018/1/19
* @description: 權限配置
*/
@Configuration
public class WebSecurityConfig extends AbstractSecurityConfig {
}
UserDetailsService 實現(xiàn)
/**
* @author: 林塬
* @date: 2018/1/9
* @description: 用戶信息獲取
*/
@Service
public class UserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService {
/**
* 通過 Username 加載用戶詳情
* @param username 用戶名
* @return UserDetails
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (username.equals("linyuan")) {
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
String password = passwordEncoder.encode("123456");
UserDetails userDetails = new User("linyuan",
password,
AuthorityUtils.commaSeparatedStringToAuthorityList(AuthoritiesEnum.USER.getRole()));
return userDetails;
}
return null;
}
}
啟動類
@SpringBootApplication
@EnableAuthJWTTokenStore // 使用 JWT 存儲令牌
//@EnableDBClientDetailsService //從 JDBC 加載客戶端詳情,需配置在啟動類上,若在子類上會出現(xiàn)順序問題,導致 Bean 創(chuàng)建失敗
public class AuthenticationServerApplication {
public static void main(String[] args) {
SpringApplication.run(AuthenticationServerApplication.class, args);
}
}
微服務配置
server:
port: 9005
encrypt:
key-store:
location: mytest.jks
secret: mypass
alias: mytest
# 若從數(shù)據(jù)庫中獲取客戶端信息則需配置數(shù)據(jù)庫源
#spring:
# datasource:
# driver-class-name: org.h2.Driver
# url: jdbc:h2:mem:test
# username: sa
# h2:
# console:
# enabled: true
單元測試
/**
* @author: 林塬
* @date: 2018/1/16
* @description: 令牌單元測試
*/
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = AuthenticationServerApplication.class)
@AutoConfigureMockMvc
public class TokenControllerTest {
@Autowired
private MockMvc mockMvc;
/**
* 密碼授權模式獲取令牌
*
* @throws Exception
*/
@Test
public void getToken() throws Exception {
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.put("username", Collections.singletonList("linyuan"));
map.put("password", Collections.singletonList("123456"));
map.put("grant_type", Collections.singletonList("password"));
map.put("scope", Collections.singletonList("read"));
int status = this.mockMvc.perform(
post("/oauth/token")
.header("Authorization", "Basic " + Base64.getEncoder().encodeToString("resource1:secret".getBytes()))
.params(map)
.contentType(MediaType.MULTIPART_FORM_DATA)
.accept(MediaType.APPLICATION_JSON)
).andDo(print()).andReturn().getResponse().getStatus();
switch (status) {
case HttpStatus.SC_OK:
log.info("密碼授權模式獲取令牌---------------->成功(200)");
break;
case HttpStatus.SC_UNAUTHORIZED:
log.info("密碼授權模式獲取令牌---------------->失敗(401---沒有權限,請檢查驗證信息,賬號是否存在、客戶端信息)");
break;
case HttpStatus.SC_BAD_REQUEST:
log.info("密碼授權模式獲取令牌---------------->失敗(400---請求失敗,請檢查密碼是否正確)");
break;
default:
log.info("密碼授權模式獲取令牌---------------->失敗({}---未知結果)",status);
break;
}
Assert.assertEquals(status,HttpStatus.SC_OK);
}
}
resource2-server(資源服務器2)
資源服務器配置
/**
* @author: 林塬
* @date: 2018/1/11
* @description: 資源服務器訪問權限配置
*/
@Configuration
public class WebSecurityConfig extends ResServerConfig{
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
http.authorizeRequests()
//訪問受保護資源 /res 的要求:客戶端 Scope 為 read,用戶本身角色為 USER
.antMatchers("/res")
.access("#oauth2.hasScope('read') and hasRole('USER')");
}
}
受保護資源
/**
* @author: 林塬
* @date: 2018/1/16
* @description: 資源服務器2-資源接口
*/
@RestController
public class ResController {
@GetMapping("/res")
public ResponseEntity<String> res(){
return ResponseEntity.ok("<h1>這是資源服務器2的受保護的資源</h1>");
}
}
啟動類
@SpringBootApplication
@EnableResJWTTokenStore //OAuth2 使用 JWT 解析令牌
public class Resource2ServerApplication {
public static void main(String[] args) {
SpringApplication.run(Resource2ServerApplication.class, args);
}
}
微服務配置
server:
port: 9006
security:
oauth2:
resource:
jwt:
key-uri: http://localhost:9005/oauth/token_key
resource1-server(資源服務器1(客戶端))
資源服務器訪問權限配置
/**
* @author: 林塬
* @date: 2018/1/18
* @description: 資源服務器訪問權限配置
*/
@Configuration
public class WebSecurityConfig extends ResServerConfig {
@Override
public void configure(HttpSecurity http) throws Exception {
super.configure(http);
http
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/res","/res2/res")
.access("#oauth2.hasScope('read') and hasRole('USER')");
}
}
用戶登錄數(shù)據(jù)傳輸對象
/**
* @author: 林塬
* @date: 2018/1/15
* @description: 用戶登錄數(shù)據(jù)傳輸對象
*/
@Data
public class LoginDTO implements Serializable {
@NotBlank(message = "用戶名不能為空")
private String username;
@NotBlank(message = "密碼不能為空")
private String password;
}
資源服務器受保護資源
/**
* @author: 林塬
* @date: 2018/1/16
* @description: 資源服務器1-資源接口
*/
@RestController
@AllArgsConstructor
public class ResController {
private RestTemplate restTemplate;
@GetMapping("/res")
public ResponseEntity<String> res(){
return ResponseEntity.ok("<h1>這是資源服務器1的受保護的資源</h1>");
}
/**
* 訪問資源服務器2-資源接口
* @param httpReq
* @return
*/
@GetMapping("/res2/res")
public ResponseEntity<String> remoteRes(HttpServletRequest httpReq){
//HttpEntity
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("Authorization",httpReq.getHeader("Authorization"));
HttpEntity httpEntity = new HttpEntity(httpHeaders);
//請求資源服務器2的資源
return restTemplate.exchange("http://localhost:9006/res",HttpMethod.GET,httpEntity,String.class);
}
}
令牌管理接口
/**
* @author: 林塬
* @date: 2018/1/16
* @description: 令牌管理接口
*/
@RestController
@AllArgsConstructor
public class TokenController {
private OAuth2ClientProperties oAuth2ClientProperties;
private OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails;
private RestTemplate restTemplate;
/**
* 通過密碼授權方式向授權服務器獲取令牌
* @param loginDTO
* @param bindingResult
* @return
* @throws Exception
*/
@PostMapping(value = "/login")
public ResponseEntity<OAuth2AccessToken> login(@RequestBody @Valid LoginDTO loginDTO, BindingResult bindingResult) throws Exception{
if (bindingResult.hasErrors()) {
throw new Exception("登錄信息格式錯誤");
} else {
//Http Basic 驗證
String clientAndSecret = oAuth2ClientProperties.getClientId()+":"+oAuth2ClientProperties.getClientSecret();
//這里需要注意為 Basic 而非 Bearer
clientAndSecret = "Basic "+Base64.getEncoder().encodeToString(clientAndSecret.getBytes());
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("Authorization",clientAndSecret);
//授權請求信息
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
map.put("username", Collections.singletonList(loginDTO.getUsername()));
map.put("password", Collections.singletonList(loginDTO.getPassword()));
map.put("grant_type", Collections.singletonList(oAuth2ProtectedResourceDetails.getGrantType()));
map.put("scope", oAuth2ProtectedResourceDetails.getScope());
//HttpEntity
HttpEntity httpEntity = new HttpEntity(map,httpHeaders);
//獲取 Token
return restTemplate.exchange(oAuth2ProtectedResourceDetails.getAccessTokenUri(), HttpMethod.POST,httpEntity,OAuth2AccessToken.class);
}
}
}
啟動類
@SpringBootApplication
@EnableResJWTTokenStore //OAuth2 使用 JWT 解析令牌
public class Resource1ServerApplication {
@Bean
public RestTemplate restTemplate(){
return new RestTemplate();
}
public static void main(String[] args) {
SpringApplication.run(Resource1ServerApplication.class, args);
}
}
微服務配置
server:
port: 9007
security:
oauth2:
client:
clientId: resource1
clientSecret: secret
userAuthorizationUri: http://localhost:9005/oauth/authorize
grant-type: password
scope: read
access-token-uri: http://localhost:9005/oauth/token
resource:
jwt:
key-uri: http://localhost:9005/oauth/token_key
basic:
enabled: false
單元測試
/**
* @author: 林塬
* @date: 2018/1/19
* @description: 資源獲取單元測試
*/
@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = Resource1ServerApplication.class)
@AutoConfigureMockMvc
public class ResControllerTest {
@Autowired
private MockMvc mockMvc;
private ObjectMapper objectMapper = new ObjectMapper();
private OAuth2AccessToken oAuth2AccessToken;
/**
* 獲取令牌
* @throws Exception
*/
@Before
public void getToken() throws Exception {
LoginDTO loginDTO = new LoginDTO();
loginDTO.setUsername("linyuan");
loginDTO.setPassword("123456");
byte[] body = this.mockMvc.perform(
post("/login")
.content(objectMapper.writeValueAsBytes(loginDTO))
.contentType(MediaType.APPLICATION_JSON) //請求數(shù)據(jù)的格式
.accept(MediaType.APPLICATION_JSON) //接收返回數(shù)據(jù)的格式
).andExpect(status().isOk())
.andReturn().getResponse().getContentAsByteArray();
oAuth2AccessToken = objectMapper.readValue(body,OAuth2AccessToken.class);
}
/**
* 測試訪問本地受保護資源
* @throws Exception
*/
@Test
public void testGetLocalRes() throws Exception{
int status = this.mockMvc.perform(
get("/res")
.header("Authorization",OAuth2AccessToken.BEARER_TYPE+" "+oAuth2AccessToken.getValue())
.accept(MediaType.APPLICATION_JSON)
).andDo(print()).andReturn().getResponse().getStatus();
printStatus(status);
Assert.assertEquals(status,HttpStatus.SC_OK);
}
/**
* 測試訪問資源服務器2受保護資源
* @throws Exception
*/
@Test
public void testGetRes2lRes() throws Exception{
int status = this.mockMvc.perform(
get("/res2/res")
.header("Authorization",OAuth2AccessToken.BEARER_TYPE+" "+oAuth2AccessToken.getValue())
.accept(MediaType.APPLICATION_JSON)
).andDo(print()).andReturn().getResponse().getStatus();
printStatus(status);
Assert.assertEquals(status,HttpStatus.SC_OK);
}
private void printStatus(int status){
switch (status) {
case HttpStatus.SC_OK:
log.info("測試訪問受保護資源---------------->成功(200)");
break;
case HttpStatus.SC_UNAUTHORIZED:
log.info("測試訪問受保護資源---------------->失敗(401---沒有權限,請確認令牌無誤,角色權限無誤,請注意是否 Authorization 請求頭部 Basic 打成了 Bearer)");
break;
case HttpStatus.SC_BAD_REQUEST:
log.info("測試訪問受保護資源---------------->失敗(400---請求失敗,請檢查用戶密碼是否正確)");
break;
default:
log.info("測試訪問本地受保護資源---------------->失敗({}---未知結果)",status);
break;
}
}
}