这个注解是为了开启OAuth2.0的sso功能,若是咱们配置了WebSecurityConfigurerAdapter,它经过添加身份验证过滤器和身份验证(entryPoint)来加强对应的配置。若是没有的话,咱们全部的请求都会被保护,也就是说咱们的全部请求都必须通过受权认证才能够,该注解的源代码以下:html
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @EnableOAuth2Client @EnableConfigurationProperties(OAuth2SsoProperties.class) @Import({ OAuth2SsoDefaultConfiguration.class, OAuth2SsoCustomConfiguration.class, ResourceServerTokenServicesConfiguration.class }) public @interface EnableOAuth2Sso { }
咱们能够看到,这个注解包含了@EnableOAuth2Client的注解,所以它也是OAuth2.0的客户端。同时分别导入了OAuth2SsoDefaultConfiguration,OAuth2SsoCustomConfiguration ,ResourceServerTokenServicesConfigurationjava
package org.springframework.boot.autoconfigure.security.oauth2.client; /** * Configuration for OAuth2 Single Sign On (SSO). If the user only has * {@code @EnableOAuth2Sso} but not on a {@code WebSecurityConfigurerAdapter} then one is * added with all paths secured. * * @author Dave Syer * @since 1.3.0 */ @Configuration @Conditional(NeedsWebSecurityCondition.class) public class OAuth2SsoDefaultConfiguration extends WebSecurityConfigurerAdapter { private final ApplicationContext applicationContext; public OAuth2SsoDefaultConfiguration(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @Override protected void configure(HttpSecurity http) throws Exception { //拦截全部请求路径 http.antMatcher("/**").authorizeRequests().anyRequest().authenticated(); new SsoSecurityConfigurer(this.applicationContext).configure(http); } protected static class NeedsWebSecurityCondition extends EnableOAuth2SsoCondition { @Override public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) { return ConditionOutcome.inverse(super.getMatchOutcome(context, metadata)); } } }
/** * Configuration for OAuth2 Single Sign On (SSO) when there is an existing * {@link WebSecurityConfigurerAdapter} provided by the user and annotated with * {@code @EnableOAuth2Sso}. The user-provided configuration is enhanced by adding an * authentication filter and an authentication entry point. * * @author Dave Syer */ @Configuration @Conditional(EnableOAuth2SsoCondition.class) public class OAuth2SsoCustomConfiguration implements ImportAware, BeanPostProcessor, ApplicationContextAware { private Class<?> configType; private ApplicationContext applicationContext; @Override public void setApplicationContext(ApplicationContext applicationContext) { this.applicationContext = applicationContext; } @Override public void setImportMetadata(AnnotationMetadata importMetadata) { this.configType = ClassUtils.resolveClassName(importMetadata.getClassName(), null); } @Override public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { if (this.configType.isAssignableFrom(bean.getClass()) && bean instanceof WebSecurityConfigurerAdapter) { ProxyFactory factory = new ProxyFactory(); factory.setTarget(bean); factory.addAdvice(new SsoSecurityAdapter(this.applicationContext)); bean = factory.getProxy(); } return bean; } private static class SsoSecurityAdapter implements MethodInterceptor { private SsoSecurityConfigurer configurer; SsoSecurityAdapter(ApplicationContext applicationContext) { this.configurer = new SsoSecurityConfigurer(applicationContext); } @Override public Object invoke(MethodInvocation invocation) throws Throwable { if (invocation.getMethod().getName().equals("init")) { Method method = ReflectionUtils .findMethod(WebSecurityConfigurerAdapter.class, "getHttp"); ReflectionUtils.makeAccessible(method); HttpSecurity http = (HttpSecurity) ReflectionUtils.invokeMethod(method, invocation.getThis()); this.configurer.configure(http); } return invocation.proceed(); } } }
在属性文件中有几个关键点,我须要在这里说明一下,配置文件例子:git
server: port: 8081 servlet: session: cookie: name: OAUTH2SESSION spring: application: name: sport-service security: oauth2: client: clientId: root clientSecret: root accessTokenUri: http://localhost:8080/oauth/token userAuthorizationUri: http://localhost:8080/oauth/authorize pre-established-redirect-uri: http://localhost:8081/prom resource: userInfoUri: http://localhost:8080/user preferTokenInfo: false sso: login-path: /login
server.servlet.session.cookie.name
必须配置,不然会报org.springframework.security.oauth2.common.exceptions.InvalidRequestException, Possible CSRF detected - state parameter was required but no state could be found
的错误,具体能够参考:地址OAuth2ClientAuthenticationProcessingFilter
,这个类为资源服务器获取user信息的认证过滤器,源代码以下:@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { OAuth2AccessToken accessToken; try { //拿到token 若是当前环境没有存token则去accessTokenUri地址获取 accessToken = restTemplate.getAccessToken(); } catch (OAuth2Exception e) { BadCredentialsException bad = new BadCredentialsException("Could not obtain access token", e); publish(new OAuth2AuthenticationFailureEvent(bad)); throw bad; } try { //根据token加载用户资源 OAuth2Authentication result = tokenServices.loadAuthentication(accessToken.getValue()); if (authenticationDetailsSource!=null) { request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, accessToken.getValue()); request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, accessToken.getTokenType()); result.setDetails(authenticationDetailsSource.buildDetails(request)); } publish(new AuthenticationSuccessEvent(result)); return result; } catch (InvalidTokenException e) { BadCredentialsException bad = new BadCredentialsException("Could not obtain user details from token", e); publish(new OAuth2AuthenticationFailureEvent(bad)); throw bad; } }
这里面必定注意的是,若是资源服务器和认证服务器分开的话,请确保认证服务器的地址必定容许匿名访问github
说句实话,spring提供那一套白花花的登陆与受权页面我想咱们你们也不会去用吧,那么根据官网的提示咱们能够本身配置受权页面与登陆页,官网说明说下:spring
Most of the Authorization Server endpoints are used primarily by machines, but there are a couple of resource that need a UI and those are the GET for /oauth/confirm_access and the HTML response from /oauth/error. They are provided using whitelabel implementations in the framework, so most real-world instances of the Authorization Server will want to provide their own so they can control the styling and content. All you need to do is provide a Spring MVC controller with @RequestMappings for those endpoints, and the framework defaults will take a lower priority in the dispatcher. In the /oauth/confirm_access endpoint you can expect an AuthorizationRequest bound to the session carrying all the data needed to seek approval from the user (the default implementation is WhitelabelApprovalEndpoint so look there for a starting point to copy). You can grab all the data from that request and render it however you like, and then all the user needs to do is POST back to /oauth/authorize with information about approving or denying the grant. The request parameters are passed directly to a UserApprovalHandler in the AuthorizationEndpoint so you can interpret the data more or less as you please.json
概括总结一下,这里给咱们的信息:服务器
/oauth/confirm_access
这个端点用于跳转至受权页的,咱们须要提供一个SpringMVC的Controller并使用@RequestMapping注解标注,同时会将AuthorizationRequest请求绑定到Session当中来用户受权时所需的信息/oauth/error
这个端点是用于配置时的错误页面scpoe.<scopename>
,具体能够参考org.springframework.security.oauth2.provider.approval.ApprovalStoreUserApprovalHandler
类的updateAfterApproval
的方法在这里咱们看看源代码就好理解了,AuthorizationEndpoint源代码以下:cookie
@FrameworkEndpoint @SessionAttributes("authorizationRequest") public class AuthorizationEndpoint extends AbstractEndpoint { //..... private String userApprovalPage = "forward:/oauth/confirm_access"; private String errorPage = "forward:/oauth/error"; //.... 省略其余代码 @RequestMapping(value = "/oauth/authorize") public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters, SessionStatus sessionStatus, Principal principal) { //....省略其余代码 // Place auth request into the model so that it is stored in the session // for approveOrDeny to use. That way we make sure that auth request comes from the session, // so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session. model.put("authorizationRequest", authorizationRequest); return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal); } // We need explicit approval from the user. private ModelAndView getUserApprovalPageResponse(Map<String, Object> model, AuthorizationRequest authorizationRequest, Authentication principal) { logger.debug("Loading user approval page: " + userApprovalPage); model.putAll(userApprovalHandler.getUserApprovalRequest(authorizationRequest, principal)); return new ModelAndView(userApprovalPage, model); } //.....省略其余代码 }
在这里我贴出一个具体示例,可供你们参考:session
<form class="am-form tpl-form-line-form" action="/oauth/authorize" method="post"> <#list scopes as scope> <div class="am-form-group"> <h3>${scope}</h3> <label class="am-radio-inline"> <!-- name必须为scope.<scopename>,好比scope.email --> <input type="radio" name="${scope}" value="true" data-am-ucheck> 赞成 </label> <label class="am-radio-inline"> <input type="radio" name="${scope}" value="false" data-am-ucheck> 拒绝 </label> </div> </#list> <div class="am-form-group"> <div class="am-u-sm-9 am-u-sm-push-3"> <input type="submit" class="am-btn am-btn-primary tpl-btn-bg-color-success " value="验证"/> </div> </div> <#--<input type="hidden" name="_csrf" value="${_csrf??.token}">--> <!-- 此隐藏表单域必须添加--> <input name='user_oauth_approval' value='true' type='hidden'/> </form>
不过你们也能够参考SpringSecruity提供的受权页面源代码来定制化本身的页面元素app
2.3.二、定义测试类
@Controller @EnableOAuth2Sso public class IndexService { @ResponseBody @GetMapping("/prom") public String prometheus() { ThreadLocalRandom random = ThreadLocalRandom.current(); return "java_test_monitor{value=\"test\",} " + random.nextDouble(); } @ResponseBody @GetMapping("/user") public Authentication user() { return SecurityContextHolder.getContext().getAuthentication(); } }
首先咱们开启服务端,那么在先前的例子做以下更改
@SpringBootApplication @EnableAuthorizationServer @Controller public class AuthorizationServer { @GetMapping("/order") public ResponseEntity<String> order() { ResponseEntity<String> responseEntity = new ResponseEntity("order", HttpStatus.OK); return responseEntity; } @GetMapping("/free/test") public ResponseEntity<String> test() { ResponseEntity<String> responseEntity = new ResponseEntity("free", HttpStatus.OK); return responseEntity; } @GetMapping("/login") public String login() { return "login"; } @ResponseBody @GetMapping("/user") public Map<String, Object> userInfo() { OAuth2Authentication authentication = (OAuth2Authentication) SecurityContextHolder.getContext().getAuthentication(); Map<String, Object> map = new HashMap<>(); map.put("auth", authentication); return map; } @GetMapping("/oauth/confirm_access") public String confirmAccess(HttpSession session, Map<String, Object> model, HttpServletRequest request) { //在这里推荐使用AuthorizationRequest来获取scope AuthorizationRequest authorizationRequest = (AuthorizationRequest) session.getAttribute("authorizationRequest"); Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); model.put("auth", authentication); LinkedHashMap<String, String> linkedHashMap = (LinkedHashMap<String, String>) request.getAttribute("scopes"); model.put("scopes", linkedHashMap.keySet()); return "confirm_access"; } public static void main(String[] args) { SpringApplication.run(AuthorizationServer.class, args); } }
在原有的基础之上添加confirmAccess
,userInfo
,login
的方法分别用于跳转受权页,获取用户信息,及登陆页的方法
Resource的资源配置类:
```java @Configuration @EnableResourceServer public class ResourceConfigure extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .and().authorizeRequests().antMatchers("/free/**").permitAll() //静态资源过滤 .and().authorizeRequests().antMatchers("/assets/**").permitAll() .and().authorizeRequests().anyRequest().authenticated() .and().formLogin().loginPage("/login").permitAll();//必须认证事后才能够访问 } } ```
这里的变更主要是针对于静态资源的过滤,同时配置了登陆页也容许直接访问,同时权限页的配置相较以前没有太多变化。
```java
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable().requestMatchers().anyRequest(). and().authorizeRequests().antMatchers("/oauth/*").authenticated(). and().formLogin().loginPage("/login").permitAll(); } } ```
当启动好服务端后,再启动客户端,两个服务启动完毕后。咱们根据上述例子,访问http://localhost:8081/prom ,而后它会跳转至服务端的登陆页进行受权。
登陆事后,会跳转到受权页
当经过受权后,会跳转到登陆页进行token的获取,登陆成功后咱们能够访问到咱们的目标地址: