Spring Security 與 OAuth2(完整案例)

個人 OAuth2 全部文章

案例簡述

簡述:

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;
        }
    }
}
最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,333評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,491評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,263評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,946評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,708評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,409評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,939評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,774評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,209評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,641評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,872評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,650評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373