作者归档:Rick

Spring Security Oauth2手动生成OAuth2AccessToken(十二)

如果接入多种登录方式,如:平台用户名密码登录、手机验证码登录,第三方登录(微信、QQ)。可以在验证通过之后在 AuthenticationSuccessHandler 中 通过 Authentication 生成手动生成token。

  • TokenGenerator.java
@Component
public class TokenGenerator {

    @Autowired
    @Qualifier("tokenServices")
    private AuthorizationServerTokenServices authorizationServerTokenServices;

    @Autowired
    private ClientDetailsService clientDetailsService;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    private static final String ACCESS_TO_REFRESH = "access_to_refresh:";

    /**
     * 表单验证完成后创建token
     * @param authentication
     * @return
     */
    public OAuth2AccessToken createToken(Authentication authentication) {
        ClientDetails clientDetails = clientDetailsService.loadClientByClientId("c");
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = (UsernamePasswordAuthenticationToken) authentication;

        TokenRequest tokenRequest = tokenRequest();

        OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);

        OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, usernamePasswordAuthenticationToken);

        OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);

        return token;
    }

    /**
     * 刷新token
     * @param accessToken
     * @return
     */
    public OAuth2AccessToken refresh(String accessToken) {
        OAuth2AccessToken _accessToken = tokenStore.readAccessToken(accessToken);
        if (Objects.nonNull(_accessToken) && !_accessToken.isExpired()) {
            return _accessToken;
        }

        String refreshToken = stringRedisTemplate.opsForValue().get(ACCESS_TO_REFRESH + accessToken);
        return authorizationServerTokenServices.refreshAccessToken(refreshToken, tokenRequest());
    }


    private TokenRequest tokenRequest() {
        ClientDetails clientDetails = clientDetailsService.loadClientByClientId("c");

        TokenRequest tokenRequest = new TokenRequest(Collections.emptyMap(),
                clientDetails.getClientId(),
                clientDetails.getScope(),
                "all");

        return tokenRequest;
    }
}
  • 测试
@SpringBootTest
public class TokenGeneratorTest {

    @Autowired
    private TokenGenerator tokenGenerator;

    @Test
    public void testCreateToken() {
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken
                = new UsernamePasswordAuthenticationToken("rick", "123456"
                , AuthorityUtils.commaSeparatedStringToAuthorityList("ADMIN, p1, p2"));
        OAuth2AccessToken token = tokenGenerator.createToken(usernamePasswordAuthenticationToken);
        System.out.println(token.getExpiration());
        System.out.println(token);
    }

    @Test
    public void testRefresh() {
        String accessToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDMwMjkyNCwiYXV0aG9yaXRpZXMiOlsicDEiLCJwMiIsIkFETUlOIl0sImp0aSI6Ik5HZ0hEYUZiZFdXeWEyTUN1ZDFGZ0N5T1gyRSIsImNsaWVudF9pZCI6ImMifQ.X0Y9jXHD4u9aszh9lXs0flnyuNBm3lJ2Swiy49TxSqw";
        OAuth2AccessToken token = tokenGenerator.refresh(accessToken);
        System.out.println(token.getValue().equals(accessToken));
    }
}

Spring Security Oauth2授权码登录+JWT token(十一)

说明

  • 授权码模式 authorization_code 需要进行登录页面验证,所以授权服务器和资源服务器需要分开部署
  • 密码模式 password 授权服务器和资源服务器可以部署在一起。

授权服务器

  • 依赖 pom.xml
<!-- spring security 4.2.15 -->
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.5.5</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 资源服务器配置 AuthorizationServer.java
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("tokenServices")
    private AuthorizationServerTokenServices authorizationServerTokenServices;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                .tokenKeyAccess("permitAll()") // oauth/token_key是公开
                .checkTokenAccess("permitAll()") // oauth/check_token公开
                .allowFormAuthenticationForClients(); //密码模式:表单认证(申请令牌)
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()// 使用in-memory存储
                .withClient("c")// client_id
                .secret("123456")//客户端密钥
                .resourceIds("res")//资源列表
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")// 允许的授权范围
                .autoApprove(false)//false跳转到授权页面
                //加上验证回调地址
                .redirectUris("http://www.baidu.com");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .userDetailsService(userDetailsService)
                .authenticationManager(authenticationManager)// 认证管理器 => 密码模式需要在认证服务器中设置 中配置AuthenticationManager
                .tokenServices(authorizationServerTokenServices)
                .authorizationCodeServices(authorizationCodeServices)
                .allowedTokenEndpointRequestMethods(HttpMethod.POST, HttpMethod.GET);
    }

    @Bean
    public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder) {
        List<AuthenticationProvider> providers = new ArrayList<>();
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService(passwordEncoder));
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        providers.add(daoAuthenticationProvider);
        return new ProviderManager(providers);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        //Admin Role
        UserDetails theUser = User.withUsername("rick")
                .password(passwordEncoder.encode("123456"))
                .roles("ADMIN", "p1").build();
        //User Role
        UserDetails theManager = User.withUsername("john")
                .password(passwordEncoder.encode("123456"))
                .roles("USER").build();
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(theUser);
        userDetailsManager.createUser(theManager);
        return userDetailsManager;
    }

    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new InMemoryAuthorizationCodeServices();
    }
  • Token配置 TokenConfig.java
@Configuration
public class TokenConfig {

    private String SIGNING_KEY = "uaa123";

    @Bean
    public TokenStore tokenStore() {
        //使用内存存储令牌(普通令牌)
//        return new InMemoryTokenStore();
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }

    // 令牌管理服务
    @Bean("tokenServices")
    public AuthorizationServerTokenServices tokenService(ClientDetailsService clientDetailsService, TokenStore tokenStore) {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService);//客户端详情服务
        service.setSupportRefreshToken(true);//支持刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略

        // 令牌增强,支持Jwt令牌
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        // 注意顺序accessTokenConverter在最后
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
        service.setTokenEnhancer(tokenEnhancerChain);

        service.setReuseRefreshToken(false); // 只对"存储"的有效,jwt_stoken无效
        service.setAccessTokenValiditySeconds(60); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }

    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (oAuth2AccessToken, oAuth2Authentication) -> {
            Map<String, Object> info = new HashMap<>();
            info.put("hello", "world");
            ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
            return oAuth2AccessToken;
        };
    }

}
  • 配置登录页面 WebSecurityConfig
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}
  • 配置文件 application.yml
server:
  port: 8081
  • 将token存储到 redis 中,需要以下两步
    添加 pom.xml 依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

修改 TokenStore

@Bean
public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
    return new RedisTokenStore(redisConnectionFactory);
}

源码地址:https://github.com/jkxyx205/spring-security-learn/tree/master/spring-security-oauth2/spring-security-oauth2-server

资源服务器

  • 依赖 pom.xml
<!-- spring security 4.2.15 -->
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.5.5</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 资源服务配置 ResourceServer.java
@Configuration
@EnableResourceServer
@EnableGlobalMethodSecurity(securedEnabled = true, jsr250Enabled=true, prePostEnabled = true)
public class ResourceServer extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // 资源id
        resources.resourceId("res")
                .stateless(true)
                .accessDeniedHandler((request, response, e) -> {
                    response.getWriter().write(e.getMessage());
                });
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        ((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)http.authorizeRequests()
                .antMatchers("/**").access("#oauth2.hasScope('all')")
                .antMatchers("/admin").hasRole("ADMIN")
                .anyRequest()).authenticated();

    }
}

  • 检查token:方式一,采用DefaultTokenServices。 配置TokenStore(必须和AuthorizationServer相同,才能解析)TokenConfig.java
@Configuration
public class TokenConfig {

    private String SIGNING_KEY = "uaa123";

    @Bean
    public TokenStore tokenStore() {
        //使用内存存储令牌(普通令牌)
//        return new InMemoryTokenStore();
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }
}
  • 检查token:方式二,配置文件的方式,采用 RemoteTokenServices (推荐)
security:
  oauth2:
    client:
      client-id: c
      client-secret: 123456
      access-token-uri: http://localhost:8081/oauth/token
      user-authorization-uri: http://localhost:8081/oauth/authorize
    resource:
      token-info-uri: http://localhost:8081/oauth/check_token
  • API接口 IndexController.java
@RestController
public class IndexController {

    @GetMapping
    public String index() {
        return "index";
    }

    @GetMapping("admin")
    public String admin(Authentication authentication, @RequestParam("access_token") String accessToken, Jwt jwt) {
        return jwt.getClaims();
    }

    @GetMapping("p1")
    @PreAuthorize("hasRole('p1')")
    public String p1(Authentication authentication, @RequestParam("access_token") String accessToken) {
        Jwt jwt = JwtHelper.decode(accessToken);
        return jwt.getClaims();
    }
}
  • 配置文件 application.yml
server:
  port: 8082

源码地址:https://github.com/jkxyx205/spring-security-learn/tree/master/spring-security-oauth2/spring-security-oauth2-resource

测试

  • 获取授权码
http://localhost:8081/oauth/authorize?response_type=code&client_id=c&redirect_uri=http://www.baidu.com&scope=all

跳转到登录页面:输入用户名密码:rick/123456;允许授权,获取地址栏授权码 code

  • 根据授权码 code 获取token
curl -X POST \
  http://localhost:8081/oauth/token \
  -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
  -F code=cP8TeB \
  -F grant_type=authorization_code \
  -F client_id=c \
  -F client_secret=123456 \
  -F redirect_uri=http://www.baidu.com \
  -F scope=all

返回 access_token

{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDI4NTg2MSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfcDEiXSwianRpIjoienFOUFZnQVpCeFdwaE9WM1hWZjNZazNkZklVIiwiY2xpZW50X2lkIjoiYyJ9.C7NQVGDgexoZ0VSpbfH_hw1eVXV7JBmB7EH9YydDQlg",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoienFOUFZnQVpCeFdwaE9WM1hWZjNZazNkZklVIiwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDU0NTAwMSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfcDEiXSwianRpIjoiRXdWOXMtYk9mdktGM1V0SFNUc2h3b285dGk0IiwiY2xpZW50X2lkIjoiYyJ9.AWTDhc8Sl4M4tTRC4AdJ1njyhQF5oAXTDNeIfAt3b1o",
    "expires_in": 59,
    "scope": "all",
    "hello": "world",
    "jti": "zqNPVgAZBxWphOV3XVf3Yk3dfIU"
}
  • 检查token
curl -X POST \
  'http://localhost:8081/oauth/check_token?token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDI5MTkyNiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfcDEiXSwianRpIjoiRTgwajVrclhSNElHZmdvUmV1UXlrc29xQ2NvIiwiY2xpZW50X2lkIjoiYyJ9.xj_Ui_La2XjrUeSbufXunobiyzbevzPOppcknJig7d0'
  • 携带 access_token 获取资源
curl -X GET \
  'http://localhost:8082/p1?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDI4NjAyMSwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiIsIlJPTEVfcDEiXSwianRpIjoibDVVcHF3UUlBWDBMRFVQYWg1T1Q1RDdrVFgwIiwiY2xpZW50X2lkIjoiYyJ9.pknFynXTl8koIUATnoShKbQSOZnwgmP8g4943lfHK7w'

sharp-database中在Controller中使用TableGridService获取数据

TableGridService 依赖于工具类 GridUtils,可以在 Controller 层,通过SQL构造对象,直接从 HttpServletRequest 获取请求参数。

@GetMapping("request")
public Grid request(HttpServletRequest request) {
    return new DefaultTableGridService("SELECT id, title, work_time FROM t_demo WHERE title like :title")
            .list(request);
}

也可以手动指定查询count的SQL,提高查询效率。

@GetMapping("request")
public Grid request(HttpServletRequest request) {
    return new DefaultTableGridService("SELECT id, title, work_time FROM t_demo WHERE title like :title",
    "SELECT count(*) FROM t_demo WHERE title like :title")
            .list(request);
}

Spring Security Oauth2密码登录+JWT token(十)

说明

spring-security-oauth 这个项目不赞成使用了。oauth2已经由Spring Security提供服务。Spring Security没有提供对认证服务器的支持,需要 spring-authorization-server 去支持。
https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server

The Spring Security OAuth project is deprecated. The latest OAuth 2.0 support is provided by Spring Security. See the OAuth 2.0 Migration Guide for further details.

Since Spring Security doesn’t provide Authorization Server support, migrating a Spring Security OAuth Authorization Server see https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server

虽然已经不推荐使用,但是很多的老项目还是使用的Spring Security Oauth,所以还是很有必要学习一下的。

环境搭建

  • 添加依赖
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.5.5</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 授权服务 AuthorizationServer.java
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    @Qualifier("tokenServices")
    private AuthorizationServerTokenServices authorizationServerTokenServices;

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                .tokenKeyAccess("permitAll()") // oauth/token_key是公开
                .checkTokenAccess("permitAll()") // oauth/check_token公开
                .allowFormAuthenticationForClients() //密码模式:表单认证(申请令牌)
        ;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()// 使用in-memory存储
                .withClient("c")// client_id
                .secret("123456")//客户端密钥
                .resourceIds("res")//资源列表
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")// 允许的授权范围
                .autoApprove(false)//false跳转到授权页面
                //加上验证回调地址
                .redirectUris("http://www.baidu.com");
    }
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .userDetailsService(userDetailsService)
                .authenticationManager(authenticationManager)// 认证管理器 => 密码模式需要在认证服务器中设置 中配置AuthenticationManager
                .tokenServices(authorizationServerTokenServices)
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }
}

endpoints中添加 tokenService

  • 认证服务安全配置 SecurityConfig.java
@Configuration
public class SecurityConfig {

    private String SIGNING_KEY = "uaa123";

    @Bean
    public TokenStore tokenStore() {
        //使用内存存储令牌(普通令牌)
//        return new InMemoryTokenStore();
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY); //对称秘钥,资源服务器使用该秘钥来验证
        return converter;
    }

    // 令牌管理服务
    @Bean("tokenServices")
    public AuthorizationServerTokenServices tokenService(ClientDetailsService clientDetailsService, TokenStore tokenStore) {
        DefaultTokenServices service=new DefaultTokenServices();
        service.setClientDetailsService(clientDetailsService);//客户端详情服务
        service.setSupportRefreshToken(true);//支持刷新令牌
        service.setTokenStore(tokenStore);//令牌存储策略

        // 令牌增强,支持Jwt令牌
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        // 注意顺序accessTokenConverter在最后
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(tokenEnhancer(), accessTokenConverter()));
        service.setTokenEnhancer(tokenEnhancerChain);

        service.setReuseRefreshToken(false); // 只对"存储"的有效,jwt_stoken无效
        service.setAccessTokenValiditySeconds(60); // 令牌默认有效期2小时
        service.setRefreshTokenValiditySeconds(259200); // 刷新令牌默认有效期3天
        return service;
    }

    @Bean
    public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder) {
        List<AuthenticationProvider> providers = new ArrayList<>();
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService(passwordEncoder));
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        providers.add(daoAuthenticationProvider);
        return new ProviderManager(providers);
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (oAuth2AccessToken, oAuth2Authentication) -> {
            Map<String, Object> info = new HashMap<>();
            info.put("hello", "world");
            ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(info);
            return oAuth2AccessToken;
        };
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        //Admin Role
        UserDetails theUser = User.withUsername("rick")
                .password(passwordEncoder.encode("123456"))
                .roles("ADMIN").build();
        //User Role
        UserDetails theManager = User.withUsername("john")
                .password(passwordEncoder.encode("123456"))
                .roles("USER").build();
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(theUser);
        userDetailsManager.createUser(theManager);
        return userDetailsManager;
    }
}
  • 资源服务配置 ResourceServer.java
@Configuration
@EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // 资源id
        resources.resourceId("res")
                .stateless(true)
                .accessDeniedHandler((request, response, e) -> {
                    response.getWriter().write(e.getMessage());
                });
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        ((ExpressionUrlAuthorizationConfigurer.AuthorizedUrl)http.authorizeRequests()
                .antMatchers("/admin").hasRole("ADMIN")
                .anyRequest()).authenticated();

    }
}
  • 接口 IndexController.java
@RestController
public class IndexController {

    @GetMapping
    public String index() {
        return "index";
    }

    @GetMapping("admin")
    public String admin(Authentication authentication, @RequestParam("access_token") String accessToken) {
        // 解析token
        Jwt jwt = JwtHelper.decode(accessToken);
        return jwt.getClaims();
    }
}

测试

  • Postman请求
http://localhost:8080/oauth/token \
        -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
        -F grant_type=password \
        -F client_id=c \
        -F client_secret=123456 \
        -F scope=all \
        -F username=rick \
        -F password=123456

org.springframework.security.oauth2.provider.endpoint.TokenEndpoint 处理请求。

  • 响应返回token
{
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDIxOTM1NywiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiIxN1Rfb3FXZ3BKMWdzVFQ5UEhDS3lnU0p2NXMiLCJjbGllbnRfaWQiOiJjIn0.Cx5OzE2tQKcV5Mmkrbz_WTt76Mz8ABb9VbI8IwL5wiI",
    "token_type": "bearer",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiYXRpIjoiMTdUX29xV2dwSjFnc1RUOVBIQ0t5Z1NKdjVzIiwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDQ3ODQ5NywiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiJXZi1LYzZ0bXZVei1DUHVHT2xkX2lXVFJPaXMiLCJjbGllbnRfaWQiOiJjIn0.lcA7LV6Rm64Y5Vo8T-Et4be3xgIlHmZVHFbUCLL_Tz4",
    "expires_in": 59,
    "scope": "all",
    "hello": "world",
    "jti": "17T_oqWgpJ1gsTT9PHCKygSJv5s"
}
  • 请求受保护资源

header中添加参数 Authorization

curl -X GET \
  http://localhost:8080/ \
  -H 'Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDIxOTM1NywiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiIxN1Rfb3FXZ3BKMWdzVFQ5UEhDS3lnU0p2NXMiLCJjbGllbnRfaWQiOiJjIn0.Cx5OzE2tQKcV5Mmkrbz_WTt76Mz8ABb9VbI8IwL5wiI' 

请求参数添加 access_token

curl -X GET \
  'http://localhost:8080/?access_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsicmVzIl0sInVzZXJfbmFtZSI6InJpY2siLCJzY29wZSI6WyJhbGwiXSwiaGVsbG8iOiJ3b3JsZCIsImV4cCI6MTYzNDIxOTM1NywiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiIxN1Rfb3FXZ3BKMWdzVFQ5UEhDS3lnU0p2NXMiLCJjbGllbnRfaWQiOiJjIn0.Cx5OzE2tQKcV5Mmkrbz_WTt76Mz8ABb9VbI8IwL5wiI'

参考链接

Spring Security Oauth2密码登录(九)

说明

spring-security-oauth 这个项目不赞成使用了。oauth2已经由Spring Security提供服务。Spring Security没有提供对认证服务器的支持,需要 spring-authorization-server 去支持。
https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server

The Spring Security OAuth project is deprecated. The latest OAuth 2.0 support is provided by Spring Security. See the OAuth 2.0 Migration Guide for further details.

Since Spring Security doesn’t provide Authorization Server support, migrating a Spring Security OAuth Authorization Server see https://spring.io/blog/2020/04/15/announcing-the-spring-authorization-server

虽然已经不推荐使用,但是很多的老项目还是使用的Spring Security Oauth,所以还是很有必要学习一下的。

环境搭建

  • 添加依赖
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.5.5</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
  • 授权服务 AuthorizationServer.java
@Configuration
@EnableAuthorizationServer
public class AuthorizationServer extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                .tokenKeyAccess("permitAll()") // oauth/token_key是公开
                .checkTokenAccess("permitAll()") // oauth/check_token公开
                .allowFormAuthenticationForClients() // 密码模式:表单认证(申请令牌)
        ;
    }

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()// 使用in-memory存储
                .withClient("c")// client_id
                .secret("123456")//客户端密钥
                .resourceIds("res")//资源列表
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")// 该client允许的授权类型authorization_code,password,refresh_token,implicit,client_credentials
                .scopes("all")// 允许的授权范围
                .autoApprove(false)//false跳转到授权页面
                //加上验证回调地址
                .redirectUris("http://www.baidu.com");
    }
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .userDetailsService(userDetailsService)
                .authenticationManager(authenticationManager)// 认证管理器 => 密码模式需要在认证服务器中设置 中配置AuthenticationManager
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }

    @Bean
    public AuthenticationManager authenticationManager(PasswordEncoder passwordEncoder) {
        List<AuthenticationProvider> providers = new ArrayList<>();
        DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService(passwordEncoder));
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder);
        providers.add(daoAuthenticationProvider);
        return new ProviderManager(providers);
    }

    @Bean
    public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) {
        //Admin Role
        UserDetails theUser = User.withUsername("rick")
                .password(passwordEncoder.encode("123456"))
                .roles("ADMIN").build();
        //User Role
        UserDetails theManager = User.withUsername("john")
                .password(passwordEncoder.encode("123456"))
                .roles("USER").build();
        InMemoryUserDetailsManager userDetailsManager = new InMemoryUserDetailsManager();
        userDetailsManager.createUser(theUser);
        userDetailsManager.createUser(theManager);
        return userDetailsManager;
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
}
  • 资源服务配置 ResourceServer.java
@Configuration
@EnableResourceServer
public class ResourceServer extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        // 资源id
        resources.resourceId("res")
                .stateless(true)
                .accessDeniedHandler((request, response, e) -> {
                    response.getWriter().write(e.getMessage());
                });
    }


    @Override
    public void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}
  • 接口 IndexController.java
@RestController
public class IndexController {

    @GetMapping
    public String index() {
        return "index";
    }
}

测试

  • Postman请求
http://localhost:8080/oauth/token \
        -H 'content-type: multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW' \
        -F grant_type=password \
        -F client_id=c \
        -F client_secret=123456 \
        -F scope=all \
        -F username=rick \
        -F password=123456

org.springframework.security.oauth2.provider.endpoint.TokenEndpoint 处理请求。

  • 响应返回token
{
    "access_token": "1QRBR3bR2xY0hq7zPIrGoXhuz8Q",
    "token_type": "bearer",
    "refresh_token": "R85zWIbZJUIlY5dOco2NuR-sboM",
    "expires_in": 43199,
    "scope": "all"
}
  • 请求受保护资源

header中添加参数 Authorization

curl -X GET \
  http://localhost:8080/ \
  -H 'Authorization: Bearer 1QRBR3bR2xY0hq7zPIrGoXhuz8Q' 

请求参数添加 access_token

curl -X GET \
  'http://localhost:8080/?access_token=1QRBR3bR2xY0hq7zPIrGoXhuz8Q'

参考链接