1.配置認證服務器
(1) 首先配置springsecurity,其實他底層是很多filter組成,順序是請求先到他這里進行校驗,然后在到oauth
/** * @author: gaoyang * @Description: 身份認證攔截 */ @Order(1) @Configuration //注解權限攔截 @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserDetailsServiceConfig userDetailsServiceConfig; //認證服務器需配合Security使用 @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); }
//websecurity用戶密碼和認證服務器客戶端密碼都需要加密算法 @Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //驗證用戶權限 auth.userDetailsService(userDetailsServiceConfig); //也可以在內存中創建用戶並為密碼加密 // auth.inMemoryAuthentication() // .withUser("user").password(passwordEncoder().encode("123")).roles("USER") // .and() // .withUser("admin").password(passwordEncoder().encode("123")).roles("ADMIN"); } //uri權限攔截,生產可以設置為啟動動態讀取數據庫,具體百度 @Override protected void configure(HttpSecurity http) throws Exception { http //此處不要禁止formLogin,code模式測試需要開啟表單登陸,並且/oauth/token不要放開或放入下面ignoring,因為獲取token首先需要登陸狀態 .formLogin() .and() .csrf().disable() .authorizeRequests().antMatchers("/test").permitAll() .and() .authorizeRequests().anyRequest().authenticated(); } //設置不攔截資源服務器的認證請求 @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/oauth/check_token"); } }
(2)這里的UserDetailsServiceConfig就是去校驗登陸用戶,可以寫測試使用內存或者數據庫方式讀取用戶信息(我這里寫死了賬號為user,密碼為123)
@Component public class UserDetailsServiceConfig implements UserDetailsService { @Autowired private PasswordEncoder passwordEncoder; //生產環境使用數據庫進行驗證 @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { if (!username.equals("user")) { throw new AcceptPendingException(); } return new User(username, passwordEncoder.encode("123"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER")); } }
(3)配置認證服務器(詳見注釋)
/** * @author: gaoyang * @Description:認證服務器配置 */ @Order(2) @EnableAuthorizationServer @Configuration public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired AuthenticationManager authenticationManager; @Autowired BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired UserDetailsServiceConfig myUserDetailsService; //為了測試客戶端與憑證存儲在內存(生產應該用數據庫來存儲,oauth有標准數據庫模板) @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("client1-code") // client_id .secret(bCryptPasswordEncoder.encode("123")) // client_secret .authorizedGrantTypes("authorization_code") // 該client允許的授權類型 .scopes("app") // 允許的授權范圍 .redirectUris("https://www.baidu.com") .resourceIds("goods", "mechant") //資源服務器id,需要與資源服務器對應 .and() .withClient("client2-credentials") .secret(bCryptPasswordEncoder.encode("123")) .authorizedGrantTypes("client_credentials") .scopes("app") .resourceIds("goods", "mechant") .and() .withClient("client3-password") .secret(bCryptPasswordEncoder.encode("123")) .authorizedGrantTypes("password") .scopes("app") .resourceIds("mechant") .and() .withClient("client4-implicit") .authorizedGrantTypes("implicit") .scopes("app") .resourceIds("mechant"); } //配置token倉庫 @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //authenticationManager配合password模式使用 endpoints.authenticationManager(authenticationManager) //這里使用內存存儲token,也可以使用redis和數據庫 .tokenStore(new InMemoryTokenStore()); endpoints.allowedTokenEndpointRequestMethods(HttpMethod.GET,HttpMethod.POST); endpoints.tokenEnhancer(new TokenEnhancer() { @Override public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) { //在返回token的時候可以加上一些自定義數據 DefaultOAuth2AccessToken token = (DefaultOAuth2AccessToken) oAuth2AccessToken; Map<String, Object> map = new LinkedHashMap<>(); map.put("nickname", "測試姓名"); token.setAdditionalInformation(map); return token; } }); } //配置token狀態查詢 @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { //開啟支持通過表單方式提交client_id和client_secret,否則請求時以basic auth方式,頭信息傳遞Authorization發送請求 security.allowFormAuthenticationForClients(); } //以下數據庫配置 /** * * @Bean * @Primary * @ConfigurationProperties(prefix = "spring.datasource") * public DataSource dataSource() { * // 配置數據源(注意,我使用的是 HikariCP 連接池),以上注解是指定數據源,否則會有沖突 * return DataSourceBuilder.create().build(); * } * * @Bean * public TokenStore tokenStore() { * // 基於 JDBC 實現,令牌保存到數據 * return new JdbcTokenStore(dataSource()); * } * * @Bean * public ClientDetailsService jdbcClientDetails() { * // 基於 JDBC 實現,需要事先在數據庫配置客戶端信息 * return new JdbcClientDetailsService(dataSource()); * } * * @Override * public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { * // 設置令牌 * endpoints.tokenStore(tokenStore()); * } * * @Override * public void configure(ClientDetailsServiceConfigurer clients) throws Exception { * // 讀取客戶端配置 * clients.withClientDetails(jdbcClientDetails()); * } * */ }
(4) 新增自定義返回認證服務器數據:(這里只做演示,沒有合理封裝)
@RestController @RequestMapping("/oauth") public class CustomResult { @Autowired private TokenEndpoint tokenEndpoint; @GetMapping("/token") public Object getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { return this.result(principal,parameters); } @PostMapping("/token") public Object postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { return this.result(principal,parameters); } public Object result(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException { ResponseEntity<OAuth2AccessToken> accessToken = tokenEndpoint.getAccessToken(principal, parameters); OAuth2AccessToken body = accessToken.getBody(); Map<String, Object> customMap = body.getAdditionalInformation(); String value = body.getValue(); OAuth2RefreshToken refreshToken = body.getRefreshToken(); Set<String> scope = body.getScope(); int expiresIn = body.getExpiresIn(); customMap.put("token",value); customMap.put("scope",scope); customMap.put("expiresIn",expiresIn); customMap.put("refreshToken",refreshToken); Map map = new HashMap(); map.put("code",0); map.put("msg","success"); map.put("data",customMap); return map; } }
(5)添加獲取token錯誤返回:(注意,客戶端信息錯誤這里是攔截不到的)
@RestControllerAdvice public class RestControllerExceptionAdvice { //判斷oauth異常,自定義返回數據 @ExceptionHandler public Object exception(OAuth2Exception e){ //if ("invalid_client".equals(errorCode)) { // return new InvalidClientException(errorMessage); // } else if ("unauthorized_client".equals(errorCode)) { // return new UnauthorizedClientException(errorMessage); // } else if ("invalid_grant".equals(errorCode)) { // return new InvalidGrantException(errorMessage); // } else if ("invalid_scope".equals(errorCode)) { // return new InvalidScopeException(errorMessage); // } else if ("invalid_token".equals(errorCode)) { // return new InvalidTokenException(errorMessage); // } else if ("invalid_request".equals(errorCode)) { // return new InvalidRequestException(errorMessage); // } else if ("redirect_uri_mismatch".equals(errorCode)) { // return new RedirectMismatchException(errorMessage); // } else if ("unsupported_grant_type".equals(errorCode)) { // return new UnsupportedGrantTypeException(errorMessage); // } else if ("unsupported_response_type".equals(errorCode)) { // return new UnsupportedResponseTypeException(errorMessage); // } else { // return (OAuth2Exception)("access_denied".equals(errorCode) ? new UserDeniedAuthorizationException(errorMessage) : new OAuth2Exception(errorMessage)); // } return "獲取token錯誤"; } }
(6)添加自定義登陸及授權頁面:
@Controller // 必須配置該作用域設置 @SessionAttributes("authorizationRequest") public class Oauth2Controller { private RequestCache requestCache = new HttpSessionRequestCache(); private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy(); @RequestMapping("/authentication/require") @ResponseBody @ResponseStatus(code = HttpStatus.UNAUTHORIZED) public Map requireAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException { SavedRequest savedRequest = requestCache.getRequest(request, response); if (null != savedRequest) { String targetUrl = savedRequest.getRedirectUrl(); System.out.println("引發跳轉的請求是:" + targetUrl); redirectStrategy.sendRedirect(request, response, "/ologin"); } //如果訪問的是接口資源 return new HashMap() {{ put("code", 401); put("msg", "訪問的服務需要身份認證,請引導用戶到登錄頁"); }}; } @RequestMapping("/ologin") public String oauthLogin(){ return "oauthLogin"; } //授權控制器 @RequestMapping("/oauth/confirm_access") public String getAccessConfirmation(Map<String, Object> model, HttpServletRequest request) throws Exception { Map<String, String> scopes = (Map<String, String>) (model.containsKey("scopes") ? model.get("scopes") : request.getAttribute("scopes")); List<String> scopeList = new ArrayList<>(); if (scopes != null) { scopeList.addAll(scopes.keySet()); } model.put("scopeList", scopeList); return "oauthGrant"; } }
//uri權限攔截,生產可以設置為啟動動態讀取數據庫,具體百度 @Override protected void configure(HttpSecurity http) throws Exception { http //此處不要禁止formLogin,code模式測試需要開啟表單登陸,並且/oauth/token不要放開或放入下面ignoring,因為獲取token首先需要登陸狀態 .formLogin().loginPage("/authentication/require") .loginProcessingUrl("/authentication/form") .passwordParameter("password") .usernameParameter("username") .and() .csrf().disable() .authorizeRequests().antMatchers("/test","/authentication/require","/ologin").permitAll() .and() .authorizeRequests().anyRequest().authenticated(); }
4.配置資源服務器
(1)配置
//配置資源服務器 @Configuration @EnableResourceServer @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true) public class ResourceServerConfig extends ResourceServerConfigurerAdapter { private ObjectMapper objectMapper = new ObjectMapper(); @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { //設置資源服務器id,需要與認證服務器對應 resources.resourceId("mechant"); //當權限不足時返回 resources.accessDeniedHandler((request, response, e) -> { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter() .write(objectMapper.writeValueAsString(Result.from("0001", "權限不足", null))); }); //當token不正確時返回 resources.authenticationEntryPoint((request, response, e) -> { response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); response.getWriter() .write(objectMapper.writeValueAsString(Result.from("0002", "access_token錯誤", null))); }); } //配置uri攔截策略 @Override public void configure(HttpSecurity http) throws Exception { http .csrf().disable() .httpBasic().disable() .exceptionHandling() .authenticationEntryPoint((req, resp, exception) -> { resp.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); resp.getWriter() .write(objectMapper.writeValueAsString(Result.from("0002", "沒有攜帶token", null))); }) .and() //無需登陸 .authorizeRequests().antMatchers("/noauth").permitAll() .and() //攔截所有請求,並且檢查sope .authorizeRequests().anyRequest().access("isAuthenticated() && #oauth2.hasScope('app')"); } //靜態內部返回類 @Data static class Result<T> { private String code; private String msg; private T data; public Result(String code, String msg, T data) { this.code = code; this.msg = msg; this.data = data; } public static <T> Result from(String code, String msg, T data) { return new Result(code, msg, data); } } }
(2)測試接口
@RestController public class TestController { @GetMapping("ping") public Object test() { return "pong"; } //無需登陸 @GetMapping("noauth") public Object noauth() { return "noauth"; } }
(3)application.yml配置(遠程向認證服務器鑒權)
#配置向認證服務器認證權限 security: oauth2: client: client-id: client3-password client-secret: 123 access-token-uri: http://localhost:8082/oauth/token user-authorization-uri: http://localhost:8082/oauth/authorize resource: token-info-uri: http://localhost:8082/oauth/check_token
5.測試用例~
(1)password模式
表單方式:(localhost:8082/oauth/token?username=user&password=123&grant_type=password&client_secret=123&client_id=client3-password)
注意需要開啟認證服務器的:
@Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { //開啟支持通過表單方式提交client_id和client_secret,否則請求時以basic auth方式,頭信息傳遞Authorization發送請求 security.allowFormAuthenticationForClients(); }
表單加token方式:
(2)code模式
瀏覽器訪問: localhost:8082/oauth/authorize?client_id=client1-code&response_type=code
跳轉到登陸頁面:
選擇允許
然后跳轉到之前設置的地址,並攜帶code:
拿着code請求token:
自定義登陸及授權頁面:
## 當前這樣配置的話,如果各微服務之間互相調用,則是沒有權限的;所以我們可以給他加上token,例如使用feign:
@Component public class OauthConfig implements RequestInterceptor { @Override public void apply(RequestTemplate requestTemplate) { requestTemplate.query("access_token","71f422b5-2204-4653-8a85-9cf2c62aac81"); } }
這里我寫固定了,其實實現的話可以在認證服務器指定一個客戶端模式,然后去動態獲取token,這個token的過期緩存等等,可以自由發揮;
其實現在很多微服務都是內網通信,通過路由暴露端口了,所以可以定制一些特殊請求,來做無權限訪問?