springboot從原理上實現oauth2
一直以來,使用spring-cloud-starter-oauth2進行用戶認證,然而只要配置一些配置文件,寫一個登陸頁,就可以進行簡單的使用。這樣會造成我們無法理解oauth2的工作原理,從哪跳轉到哪,又在哪裏做驗證。這樣會導致oauth2用起來感覺莫名奇妙,也無法體會到它的安全性。
從網上找了一幅圖,就是整個oauth2.0的協議實現原理。
代碼
1.springboot客戶端依賴:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.client</artifactId>
<version>0.31</version>
</dependency>
</dependencies>
2.springboot服務端依賴:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.authzserver</artifactId>
<version>0.31</version>
</dependency>
<dependency>
<groupId>org.apache.oltu.oauth2</groupId>
<artifactId>org.apache.oltu.oauth2.resourceserver</artifactId>
<version>0.31</version>
</dependency>
</dependencies>
這裏用的springboot版本是2.0.3
3.在客戶端web項目中構造一個oauth的客戶端請求對象(OAuthClientRequest),在此對象中攜帶客戶端信息(clientId、accessTokenUrl、response_type、redirectUrl),將此信息放入http請求中,重定向到服務端。此步驟對應上圖1
ServerController.java
@RequestMapping("/requestServerCode")
public String requestServerFirst(HttpServletRequest request, HttpServletResponse response, RedirectAttributes attr)throws OAuthProblemException {
clientId = "clientId";
clientSecret = "clientSecret";
accessTokenUrl = "responseCode";
userInfoUrl = "userInfoUrl";
redirectUrl = "http://localhost:8081/oauthclient01/server/callbackCode";
responseType = "code";
OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
String requestUrl = null;
try{
OAuthClientRequest accessTokenRequest = OAuthClientRequest
.authorizationLocation(accessTokenUrl)
.setResponseType(responseType)
.setClientId(clientId)
.setRedirectURI(redirectUrl)
.buildQueryMessage();
requestUrl = accessTokenRequest.getLocationUri();
System.out.println(requestUrl);
}catch (OAuthSystemException e){
e.printStackTrace();
}
return "redirect:http://localhost:8082/oauthserver/" + requestUrl;
}
此段代碼對應開發步驟1.其中accessTokenUrl是服務端返回code的controller方法映射地址。redirectUrl是告訴服務端,code要傳回客戶端的一個controller方法,該方法的映射地址就是redirectUrl。
4.在服務端web項目中接受第一步傳過來的request,從中獲取客戶端信息,可以自行驗證信息的可靠性。同時構造一個oauth的code授權許可對象(OAuthAuthorizationResponseBuilder),並在其中設置授權碼code,將此對象傳回客戶端。此步驟對應上圖2
AuthorizeController.java
@RequestMapping("/responseCode")
public Object toShowUser(Model model, HttpServletRequest request){
System.out.println("-------------------------服務端/responseCode---------------------------------------------------");
try{
OAuthAuthzRequest oauthRequest = new OAuthAuthzRequest(request);
if(oauthRequest.getClientId() != null && oauthRequest.getClientId() != ""){
String authorizationCode = "authorizationCode";
String responseType = oauthRequest.getParam(OAuth.OAUTH_RESPONSE_TYPE);
OAuthASResponse.OAuthAuthorizationResponseBuilder builder = OAuthASResponse.authorizationResponse(request, HttpServletResponse.SC_FOUND);
builder.setCode(authorizationCode);
String redirectURI = oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);
final OAuthResponse response = builder.location(redirectURI).buildQueryMessage();
System.out.println("服務的/responseCode內,返回的回調路徑:" + response.getLocationUri());
System.out.println("----------------------服務端/responseCode-----------------------------------------------");
String responseUri = response.getLocationUri();
HttpHeaders headers = new HttpHeaders();
try{
headers.setLocation(new URI(response.getLocationUri()));
}catch(URISyntaxException e){
e.printStackTrace();
}
return "redirect:" + responseUri;
}
}catch (OAuthSystemException e) {
e.printStackTrace();
} catch (OAuthProblemException e) {
e.printStackTrace();
}
System.out.println("---------------------服務端/responseCode-----------------------------------------------------");
return null;
}
5.在客戶端web項目中接受第二步的請求request,從中獲得code。同時構造一個oauth的客戶端請求對象(OAuthClientRequest),此次在此對象中不僅要攜帶客戶端信息(clientId、accessTokenUrl、clientSecret、GrantType、redirectUrl),還要攜帶接受到的code。再構造一個客戶端請求工具對象(oAuthClient),這個工具封裝了httpclient,用此對象將這些信息以post(一定要設置成post)的方式請求到服務端,目的是爲了讓服務端返回資源訪問令牌。此步驟對應上圖3。(另外oAuthClient請求服務端以後,會自行接受服務端的響應信息。
ServerController.java
@RequestMapping("/callbackCode")
public Object toLogin(HttpServletRequest request)throws OAuthProblemException{
System.out.println("-----------------客戶端/callbackCode--------------------------------------------------------------------");
clientId = "clientId";
clientSecret = "clientSecret";
accessTokenUrl = "http://localhost:8082/oauthserver/responseAccessToken";
userInfoUrl = "userInfoUrl";
redirectUrl = "http://localhost:8081/oauthclient01/server/accessToken";
code = request.getParameter("code");
System.out.println(code);
OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
try{
OAuthClientRequest accessTokenRequest = OAuthClientRequest
.tokenLocation(accessTokenUrl)
.setGrantType(GrantType.AUTHORIZATION_CODE)
.setClientId(clientId)
.setClientSecret(clientSecret)
.setCode(code)
.setRedirectURI(redirectUrl)
.buildQueryMessage();
OAuthAccessTokenResponse oAuthResponse = oAuthClient.accessToken(accessTokenRequest, OAuth.HttpMethod.POST);
String accessToken = oAuthResponse.getAccessToken();
// 獲取時間用於查看token是否過期Long expiresln = oAuthResponse.getExpiresIn();
System.out.println("客戶端/callbackCode方法的token:::" + accessToken);
System.out.println("--------------客戶端/callbackCode------------------------------------------------------------------");
return "redirect:http://localhost:8081/oauthclient01/server/accessToken?accessToken=" + accessToken;
}catch (OAuthSystemException e){
e.printStackTrace();
}
return null;
}
6.在服務端web項目中接受第三步傳過來的request,從中獲取客戶端信息和code,並自行驗證。再按照自己項目的要求生成訪問令牌(accesstoken),同時構造一個oauth響應對象(OAuthASResponse),攜帶生成的訪問指令(accesstoken),返回給第三步中客戶端的oAuthClient。oAuthClient接受響應之後獲取accesstoken,此步驟對應上圖4
AccessTokenController.java
@RequestMapping(value = "/responseAccessToken",method = RequestMethod.POST)
public HttpEntity token(HttpServletRequest request){
System.out.println("-----------------服務端/responseAccessToken-------------------------------------");
OAuthIssuer oAuthIssuerImpl = null;
OAuthResponse response = null;
try{
OAuthTokenRequest oauthRequest = new OAuthTokenRequest(request);
String authCode = oauthRequest.getParam(OAuth.OAUTH_CODE);
String clientSecret = oauthRequest.getClientSecret();
if(clientSecret != null || clientSecret != ""){
oAuthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
final String accessToken = oAuthIssuerImpl.accessToken();
System.out.println(accessToken);
System.out.println("--000---");
response = OAuthASResponse
.tokenResponse(HttpServletResponse.SC_OK)
.setAccessToken(accessToken)
.buildJSONMessage();
}
System.out.println("----------------服務端/responseAccessToken---------------------------------------");
return new ResponseEntity(response.getBody(), HttpStatus.valueOf(response.getResponseStatus()));
}catch (OAuthSystemException e) {
e.printStackTrace();
}catch (OAuthProblemException e) {
e.printStackTrace();
}
System.out.println("------------服務端/responseAccessToken----------------------------------------------");
return null;
}
7.此時客戶端web項目中已經有了從服務端返回過來的accesstoken,那麼在客戶端構造一個服務端資源請求對象(OAuthBearerClientRequest),在此對象中設置服務端資源請求URI,並攜帶上accesstoken。再構造一個客戶端請求工具對象(oAuthClient),用此對象去服務端靠accesstoken換取資源。此步驟對應上圖5
在服務端web項目中接受第五步傳過來的request,從中獲取accesstoken並自行驗證。之後就可以將客戶端請求的資源返回給客戶端了。
ServerController.java
@RequestMapping("/accessToken")
public ModelAndView accessToken(String accessToken){
System.out.println("-----------------客戶端/accessToken---------------------------------------------------------------------");
userInfoUrl = "http://localhost:8082/oauthserver/userInfo";
System.out.println("accessToken");
OAuthClient oAuthClient = new OAuthClient(new URLConnectionClient());
try{
OAuthClientRequest userInfoRequest = new OAuthBearerClientRequest(userInfoUrl)
.setAccessToken(accessToken).buildQueryMessage();
OAuthResourceResponse resourceResponse = oAuthClient.resource(userInfoRequest,OAuth.HttpMethod.GET,OAuthResourceResponse.class);
String username = resourceResponse.getBody();
System.out.println(username);
ModelAndView modelAndView = new ModelAndView("usernamePage");
modelAndView.addObject("username",username);
System.out.println("---------------------客戶端/accessToken-------------------------------------------------------------");
return modelAndView;
}catch (OAuthSystemException e){
e.printStackTrace();
}catch (OAuthProblemException e){
e.printStackTrace();
}
System.out.println("-------------------客戶端/accessToken------------------------------------------------------------------");
return null;
}
UserInfoController.java
@RequestMapping("/userInfo")
public HttpEntity userInfo(HttpServletRequest request)throws OAuthSystemException{
System.out.println("--------------------服務端/userInfo----------------------------------------------");
try{
OAuthAccessResourceRequest oauthRequest = new OAuthAccessResourceRequest(request, ParameterStyle.QUERY);
String accessToken = oauthRequest.getAccessToken();
System.out.println("accessToken");
User user = new User();
user.setUsername("tom");
user.setPassword("123456");
String username = accessToken+"---"+Math.random()+"----"+user.getUsername();
System.out.println(username);
System.out.println("服務端/userInfo::::::ppp");
System.out.println("-----------服務端/userInfo----------------------------------------------------------");
return new ResponseEntity(username, HttpStatus.OK);
}catch(OAuthProblemException e){
e.printStackTrace();
String errorCode = e.getError();
if (OAuthUtils.isEmpty(errorCode)) {
OAuthResponse oauthResponse = OAuthRSResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.buildHeaderMessage();
HttpHeaders headers = new HttpHeaders();
headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,
oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
return new ResponseEntity(headers, HttpStatus.UNAUTHORIZED);
}
OAuthResponse oauthResponse = OAuthRSResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setError(e.getError())
.setErrorDescription(e.getDescription())
.setErrorUri(e.getUri())
.buildHeaderMessage();
HttpHeaders headers = new HttpHeaders();
headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,
oauthResponse.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
System.out.println("-----------服務端/userInfo------------------------------------------------------------------------------");
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
}
另外:項目中有些參數是自己指定,然後認證服務端是不需要寫跳轉的IP,這也是oauth2的精髓,需要跳轉的地方都有客戶端提供,項目中生成的accessToken可以當成token使用,這樣其他服務就不需要再認證,只要判斷是否由此token就可以了。
如果想要深入研究底層原理的話,推薦兩個博客:
阮一峯
張開濤
那還有同學問了,如果我的客戶端不是java,而是nginx咋辦,那麼你可以看我的另外一篇博客。
https://blog.csdn.net/lwy572039941/article/details/102397704
項目地址:
https://download.csdn.net/download/lwy572039941/11835481