在上一篇《OAuth 2.0 授权码请求》中我们已经可以获取到access_token了,本节将使用客户端来访问远程资源 配置资源服务器 授权服务器负责生成并发放访问令牌(access_token),客户端在访问受保护的资源时会带上访问令牌,资源服务器需要解析并验证客户端带的这个访问令牌。 如果你的资源服务器同时也是一个授权服务器(资源服务器和授权服务器在一起),那么资源服务器就不需要考虑令牌解析的事情了,否则这一步是不可或缺的。 To use the access token you need a Resource Server (which can be the same as the Authorization Server). Creating a Resource Server is easy, just add @EnableResourceServer and provide some configuration to allow the server to decode access tokens. If your application is also an Authorization Server it already knows how to decode tokens, so there is nothing else to do. If your app is a standalone service then you need to give it some more configuration. 同时,把它们放在一起的话还有一个问题需要注意,我们知道过滤器是顺序执行的,因此需要确保那些通过访问令牌来访问的资源路径不能被主过滤拦下了,需要单独摘出来。 Note: if your Authorization Server is also a Resource Server then there is another security filter chain with lower priority controlling the API resources. Fo those requests to be protected by access tokens you need their paths not to be matched by the ones in the main user-facing filter chain, so be sure to include a request matcher that picks out only non-API resources in the WebSecurityConfigurer above. 关于Spring Security中过滤器的顺序可以参见 https://docs.spring.io/spring-security/site/docs/5.0.6.RELEASE/reference/htmlsingle/#filter-ordering 这里偷个懒将它们放在一起: 复制代码 package com.cjs.example.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.expression.OAuth2WebSecurityExpressionHandler; @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { super.configure(resources); } /** * 用于配置对受保护的资源的访问规则 * 默认情况下所有不在/oauth/**下的资源都是受保护的资源 * {@link OAuth2WebSecurityExpressionHandler} */ @Override public void configure(HttpSecurity http) throws Exception { http.requestMatchers().antMatchers("/haha/**") .and() .authorizeRequests() .anyRequest().authenticated(); } } 复制代码 这里配置很简洁,很多都用了默认的设置(比如:resourceId,accessDeniedHandler,sessionManagement等等,具体可参见源码) 接下来,看看本例中我们被保护的资源,简单的几个资源(都以/haha开头),只为测试: 复制代码 package com.cjs.example.controller; import com.cjs.example.domain.UserInfo; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.security.Principal; @RestController @RequestMapping("/haha") public class MainController { @GetMapping("/sayHello") public String sayHello(String name) { return "Hello, " + name; } @PreAuthorize("hasAnyRole('ADMIN')") @RequestMapping("/sayHi") public String sayHi() { return "hahaha"; } @RequestMapping("/userInfo") public UserInfo userInfo(Principal principal) { UserInfo userInfo = new UserInfo(); userInfo.setName(principal.getName()); return userInfo; } } 复制代码 授权服务器配置 复制代码 package com.cjs.example.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer; import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer; import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private RedisConnectionFactory connectionFactory; @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { super.configure(security); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("my-client-1") .secret("$2a$10$0jyHr4rGRdQw.X9mrLkVROdQI8.qnWJ1Sl8ly.yzK0bp06aaAkL9W") .authorizedGrantTypes("authorization_code", "refresh_token") .scopes("read", "write", "execute") .redirectUris("http://localhost:8081/login/oauth2/code/callback"); // .redirectUris("http://www.baidu.com"); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore()); } @Bean public TokenStore tokenStore() { return new RedisTokenStore(connectionFactory); } public static void main(String[] args) { System.out.println(new org.apache.tomcat.util.codec.binary.Base64().encodeAsString("my-client-1:12345678".getBytes())); System.out.println(java.util.Base64.getEncoder().encodeToString("my-client-1:12345678".getBytes())); } } 复制代码 和之前相比,我们增加了TokenStore,将Token存储到Redis中。否则默认放在内存中的话每次重启的话token都丢了。下面是一个例子: application.yml如下: 复制代码 server: port: 8080 spring: redis: host: 127.0.0.1 port: 6379 logging: level: root: debug org.springframework.web: debug org.springframework.security: debug 复制代码 WebSecurity配置 我们有了资源,有了授权,我们还缺少用户。WebSecurity主要是配置咱们这个项目的一些安全配置,比如用户、认证、授权等等。 复制代码 package com.cjs.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("zhangsan") .password("$2a$10$qsJ/Oy1RmUxFA.YtDT8RJ.Y2kU3U4z0jvd35YmiMOAPpD.nZUIRMC") .roles("USER") .and() .withUser("lisi") .password("$2a$10$qsJ/Oy1RmUxFA.YtDT8RJ.Y2kU3U4z0jvd35YmiMOAPpD.nZUIRMC") .roles("USER", "ADMIN"); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/plugins/**", "/favicon.ico"); } @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } public static void main(String[] args) { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); System.out.println(bCryptPasswordEncoder.encode("123456")); System.out.println(bCryptPasswordEncoder.encode("12345678")); } } 复制代码 这里多说两句,关于Endpoint和HttpSecurity Endpoint 有很多端点我们是可以重写的,比如:/login,/oauth/token等等 HttpSecurity 很多初学者可能会不知道怎么配置HttpSecurity,这个时候其实最好的方法就是看代码或者API文档 下面一起看一下常见的几个配置 我们先来看一下,当我们继承WebSecurityConfigurerAdapter之后它的默认的HttpSecurity是怎么配置的: 复制代码 // @formatter:off protected void configure(HttpSecurity http) throws Exception { logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); } // @formatter:on 复制代码 可以看到,所有的请求都需要授权,并且指定登录的uri是/login,同时支持Basic认证。 requestMatchers() 这个方法是用于限定只有特定的HttpServletRequest实例才会导致该HttpSecurity被调用,当然是通过请求uri进行限定的了。它后面可以接多个匹配规则。例如: 复制代码 @Configuration @EnableWebSecurity public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .requestMatchers() .antMatchers("/api/**") .antMatchers("/oauth/**") .and() .authorizeRequests() .antMatchers("/**").hasRole("USER") .and() .httpBasic(); } /* 与上面那段等价 @Override protected void configure(HttpSecurity http) throws Exception { http .requestMatchers() .antMatchers("/api/**") .and() .requestMatchers() .antMatchers("/oauth/**") .and() .authorizeRequests() .antMatchers("/**").hasRole("USER") .and() .httpBasic(); } */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER"); } } 复制代码 formLogin() 该方法是用于配置登录相关的设置的。例如: 复制代码 @Configuration @EnableWebSecurity public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() .usernameParameter("username") // default is username .passwordParameter("password") // default is password .loginPage("/authentication/login") // default is /login with an HTTP get .failureUrl("/authentication/login?failed") // default is /login?error .loginProcessingUrl("/authentication/login/process"); // default is /login } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); } } 复制代码 当我们没有配置登录的时候,会用默认的登录,有默认的登录页面,还有好多默认的登录配置。具体可参见 FormLoginConfigurer.loginPage(String)方法 authorizeRequests() 该方法允许基于HttpServletRequest进行访问限制,比如角色、权限。例如: 复制代码 @Configuration @EnableWebSecurity public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/**").hasRole("USER").and().formLogin(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("user").password("password").roles("USER") .and().withUser("admin").password("password").roles("ADMIN", "USER"); } } 复制代码 anyRequest()表示匹配任意请求 authenticated()表示只有认证通过的用户才可以访问 更多可以参见API文档:https://docs.spring.io/spring-security/site/docs/5.0.6.RELEASE/api/ 用Postman访问资源 获取授权码 在浏览器中输入http://localhost:8080/oauth/authorize?response_type=code&client_id=my-client-1&redirect_uri=http://www.baidu.com&scope=read 然后跳到登录页面,输入用户名和密码登录,然后从重定向url中拿到code 换取访问令牌 访问资源 复制代码 http://localhost:8080/haha/sayHi?access_token=9f908b8f-06d6-4987-b105-665ca5a4522a { "error": "access_denied", "error_description": "不允许访问" } 这里不允许访问是因为我用zhangsan登录的