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的過期緩存等等,可以自由發揮;
其實現在很多微服務都是內網通信,通過路由暴露端口了,所以可以定制一些特殊請求,來做無權限訪問?
