Spring Security 与 OAuth2(完整案例)

个人 OAuth2 全部文章

案例简述

简述:

Demo 流程:

  • 使用 OAuth2 密码授权方式提供令牌
  • 资源服务器1(也为客户端)提供登录接口,资源所有者(用户)通过将个人账号密码提供给 资源服务器1,资源服务器1 通过该信息向授权服务器获取令牌
  • 资源服务器1(也为客户端)通过令牌(其中包含了客户端、用户等信息)访问自身受保护的资源(需要权限才能查看的资源)
  • 资源服务器2(也可资源服务器)不包含登录接口,但其提供了某些受保护的资源(需要资源服务器1带着访问令牌才能访问)
  • 资源服务器1(也为客户端)通过令牌向 资源服务器2(资源服务器) 请求其受保护的资源

使用

  • 授权服务器通过继承 AuthServerConfig 抽象类来配置授权服务器
  • 资源服务器、客户端通过继承 ResServerConfig 抽象类来配置资源服务器
  • Web 权限配置可继承 AbstractSecurityConfig 抽象类来简化配置
  • 授权服务器通过在启动类添加以下注解来设置令牌存储、客户端信息获取方式
    • @EnableAuthJWTTokenStore:使用 JWT 存储令牌
    • @EnableDBClientDetailsService:通过数据库获取客户端详情
    • @EnableDBTokenStore:通过数据库存储令牌
  • 资源服务器通过在启动类添加以下注解来设置令牌解析方式
    • @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;
    }

}

数据库方式存储令牌

/**
 * @author: 林塬
 * @date: 2018/1/22
 * @description: 使用数据库存取令牌
 */
public class DBTokenStore {

    @Autowired
    private DataSource dataSource;

    @Bean
    public TokenStore tokenStore(){
        return new JdbcTokenStore(dataSource);
    }

}

数据库方式加载客户端详情

/**
 * @author: 林塬
 * @date: 2018/1/22
 * @description: 通过数据库加载客户端详情
 */
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: 在启动类上添加该注解来----开启从数据库加载客户端详情
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(DBClientDetailsService.class)
public @interface EnableDBClientDetailsService {
}
/**
 * @author: 林塬
 * @date: 2018/1/22
 * @description: 在启动类上添加该注解来----开启通过数据库存储令牌
 *               数据库 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、内存存储令牌)
 */
@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 {

    /**
     * 调用父类构造函数,设置令牌失效日期等信息
     */
    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()                          // 使用内存存储客户端信息
                .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 实现

/**
 * @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 加载客户端详情,需配置在启动类上,若在子类上会出现顺序问题,导致 Bean 创建失败
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
# 若从数据库中获取客户端信息则需配置数据库源
#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')");
    }

}

用户登录数据传输对象

/**
 * @author: 林塬
 * @date: 2018/1/15
 * @description: 用户登录数据传输对象
 */
@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)    //请求数据的格式
                        .accept(MediaType.APPLICATION_JSON)         //接收返回数据的格式
        ).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;
        }
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342