這個註解是爲了開啓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的獲取,登陸成功後咱們能夠訪問到咱們的目標地址: