简介
- 使用Apache Oltu来做对OAuth的实现,因为它比较轻量、简单、灵活。
- 按照OAuth的角色分布,可以写三个程序:资源服务器、授权服务器和客户端Client各一个,也可以全部写到一个程序里面。
- 本例采用写成两个程序,一个服务器端,一个客户端。
数据库
- 数据库:shiro-oauth2.所在会话:mysql。
- oauth2_user:用户表,资源拥有者。admin/123456。
- oauth2_client:客户端表,存储客户端id和客户端密钥,在进行授权时使用。
服务器端
oltu源码
- 源码分包:
- issuer:生成授权码和访问令牌,刷新令牌。
- request:封装授权码请求和令牌请求的逻辑,并提供相应的校验服务。
- response:封装授权流程中的响应逻辑,提供生成不同响应结果的方法。
- validator:为request提供校验服务。
issuer
- 主要是俩接口。
- OAuthIssuer接口:默认实现类OAuthIssuerImpl。
public interface OAuthIssuer {
public String accessToken() throws OAuthSystemException;
public String authorizationCode() throws OAuthSystemException;
public String refreshToken() throws OAuthSystemException;
}
- ValueGenerator接口:默认实现类MD5Generator和UUIDValueGenerator,用于生成code和token的字符串。
public interface ValueGenerator {
public String generateValue() throws OAuthSystemException;
public String generateValue(String param) throws OAuthSystemException;
}
request
- 封装请求主要是为了提取其中的参数来验证是否合格,比如client_id是否正确。
- 主要是一个父类,其他都是其子类。
- OAuthRequest:父类,提供最基础的逻辑和方法。
- OAuthAuthzRequest:授权码请求。
- AbstractOAuthTokenRequest:抽象类,为下一个类做准备。
- OAuthTokenRequest:令牌请求。
- OAuthUnauthenticatedTokenRequest:刷新令牌的请求。
- validator部分代码分析
- 封装请求示例:
OAuthTokenRequest oAuthTokenRequest=new OAuthTokenRequest(request);
//比如规定的参数必须要有,至少GrantType、clientId、clientSecret、code、redirectUrl;
//否则抛出异常:OAuthProblemException;另有OAuthSystemException,这个较少出现
validator
- 包下all类实现自validators包的OAuthvalidator接口,其包下还有实现了all方法的AbstractValidator类。
- 本包下类:
- 部分代码分析
response
- OAuthResponse:构造响应数据的父类,构造方法protected,不能创建实例,实际用到的是其静态内部类OAuthResponseBuilder,是后面要介绍到的两个Builder类的父类;不知道在不在本包;成员如下:
其中有两个Builder:OAuthResponseBuilder和OAuthErrorResponseBuilder,后者是前者的子类。
OAuthASResponse:子类,提供了组装不同请求的方法,成员如下:
构造方法是protected,因此不能new此类的实例,我们实际需要用的只是它的两个静态内部类OAuthAuthorizationResponseBuilder和OAuthTokenResponseBuilder,通过它们的方法来生成最终的响应数据。
构造响应的方法基本也就上面框出来3个了,注意到Builder内的all方法都是返回本类的实例,也就是可以无限链式调用。我们先看一个实际使用中的场景:
// 处理授权码请求返回的响应
OAuthResponse oAuthResponse= OAuthASResponse.authorizationResponse(request, 200)
.location(redirectUrl)
.setCode(oauthCode)
.setScope(state)
.buildQueryMessage();
String url=oAuthResponse.getLocationUri();
response.sendRedirect(url);
// 令牌
OAuthResponse authASResponse = OAuthASResponse.tokenResponse(200)
.setAccessToken(access_token)
.setExpiresIn("7200")
.setRefreshToken(refreshToken)
.setTokenType(TokenType.BEARER.toString())
.setParam("re_expires_in", "14400")
.buildJSONMessage();
String json=authASResponse.getBody();
// 错误响应
OAuthResponse authASResponse = OAuthASResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setError(OAuthError.ResourceResponse.INVALID_TOKEN)
.setErrorDescription("invald expired")
.buildJSONMessage();
return new ResponseEntity(authASResponse.getBody(), headers,
HttpStatus.UNAUTHORIZED);
- buildQueryMessage():发起新请求,url即location()中的参数redirectUrl,响应体自然也送至该url。
- buildJSONMessage():返回原页面,响应体被解析为json。
- setter实际就是设置响应参数,最后调用一个buildXxxMessage方法生成一个包含所有响应参数的OAuthResponse对象(注意是OAuthResponse这个父类,而不是OAuthASResponse子类,所以最终都是要用OAuthResponse对象来接收)。
- 对象返回后就可以调用getBody(),getHeaders()之类的方法获取到其中的响应数据。
对于错误响应中的error,在set时可以直接调用alth提供的OAuthError类的常量,它们是不同场景下通用的错误标识。
参考文章
示例
- 目的:执行OAuth授权流程。
- 实体: 客户端Client、用户User。
- service:客户端的增删查改、用户的增删查改、OAuthService管理code和token。
controller:分为对内的后台管理控制器和对外的执行OAuth的控制器。后台管理主要负责QQ内部维护的用户和已在此注册的客户端,执行OAuth的控制器负责为客户端授权(给code、给token、给用户信息)。
依赖:authzserver授权服务器和resourceserver资源服务器两个依赖。
org.apache.oltu.oauth2 org.apache.oltu.oauth2.authzserver 0.31 org.apache.oltu.oauth2 org.apache.oltu.oauth2.resourceserver 0.31
授权服务器
以下两个controller一个负责根据登录表单给code,一个负责根据code给token,合为授权服务器。
AuthorizeController:授权控制器,控制客户端访问OAth服务器端的授权登录页面,通过授权验证并获取code授权码。
- 首先访问该controller第一个方法地址:/authorize?client_id=xxx&response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login,即访问授权页面。
- 控制器首先检查clientId是否正确,如果错误则返回错误响应;
- 如果正确则判断用户是否已登录,否则跳转登录页面;
- 已登录则生成相应auth code,并保存到缓存中。
- 重定向回redirect_uri:http://localhost:9080/chapter17-client/oauth2-login?code=xxx,接着客户端凭此code去获取token。
@Controller
public class AuthorizeController {
@Resource
private OAuthService oAuthService;
@Resource
private ClientService clientService;
/**
* @Author haien
* @Description 映射授权页面:/authorize?clien_id=xxx & response_type=code &
redirect_uri=http://localhost:8080/chapter17-client/oauth2-login
* @Date 2019/3/26
* @Param [model, request]
* @return java.lang.Object
**/
@RequestMapping("/authorize")
public Object authorize(Model model, HttpServletRequest request)
throws OAuthSystemException,URISyntaxException {
try {
//1. 把当前请求封装成OAth请求
OAuthAuthzRequest oAuthAuthzRequest=new OAuthAuthzRequest(request);
//2. 检查client_id是否正确
if(!oAuthService.checkClientId(oAuthAuthzRequest.getClientId())){
//错误则构造错误响应
OAuthResponse response=OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
//设置错误码
.setError(OAuthError.TokenResponse.INVALID_CLIENT)
.setErrorDescription(
Constants.INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage(); //返回原页面,响应体被前端解析为json
//获取上面构造好的响应数据
return new ResponseEntity(
response.getBody(),
HttpStatus.valueOf(response.getResponseStatus())
); //response.getBody():获取响应体,返回string;
//包含两个键值对:error和error_description
}
//3. 判断用户是否已登录
Subject subject=SecurityUtils.getSubject();
//若用户尚未登录,跳转登录页面
if(!subject.isAuthenticated()){
//如果登录失败则跳转回登录页面
if(!login(subject,request))
model.addAttribute("client",
clientService.findByClientId(
oAuthAuthzRequest.getClientId()));
//跳转登录页面
return "oauth2login";
}
//4. 登录成功或已登录则生成授权码code
String username=(String)subject.getPrincipal();
String authorizationCode=null;
/*获取请求中的response_type参数,
OAUTH_RESPONSE_TYPE值应为response_type;
responseType目前只支持code和token*/
String responseType=oAuthAuthzRequest
.getParam(OAuth.OAUTH_RESPONSE_TYPE);
if(responseType.equals(ResponseType.CODE.toString())){
//生成authCode
OAuthIssuerImpl oauthIssuerImpl=new OAuthIssuerImpl(
new MD5Generator());
authorizationCode=oauthIssuerImpl.authorizationCode();
//添加缓存条目:key=authCode.value=username
oAuthService.addAuthCode(authorizationCode,username);
}
/*5. 重定向回客户端地址,
http://localhost:8080/chapter17-client/oauth2-login?
code=xxx*/
//获取请求中的重定向地址,OAUTH_REDIRECT_URI应为redirect_uri
String redirectURI=oAuthAuthzRequest
.getParam(OAuth.OAUTH_REDIRECT_URI);
final OAuthResponse response= OAuthASResponse
.authorizationResponse(request,HttpServletResponse.SC_FOUND)
.setCode(authorizationCode) //设置授权码
.location(redirectURI)
.buildQueryMessage(); //发起新请求,url即redirectURI
//获取response数据
HttpHeaders headers=new HttpHeaders();
//响应报头域中location为重定向的url
headers.setLocation(new URI(response.getLocationUri()));
return new ResponseEntity(headers,
HttpStatus.valueOf(response.getResponseStatus()));
} catch (OAuthProblemException e) { //new OAuthAuthzRequest()抛出的
//异常处理
String redirectUri=e.getRedirectUri();
//如果客户端没有传入redirectUri
if(OAuthUtils.isEmpty(redirectUri)) {
//返回原页面,打印以下字符串
return new ResponseEntity(
"OAuth callback url needs to be provided by client!",
HttpStatus.NOT_FOUND);
}
//否则返回其他错误信息
final OAuthResponse response=OAuthASResponse
.errorResponse(HttpServletResponse.SC_FOUND)
.error(e)
.location(redirectUri)
.buildQueryMessage();
//获取response数据
HttpHeaders headers=new HttpHeaders();
headers.setLocation(new URI(response.getLocationUri()));
return new ResponseEntity(headers,
HttpStatus.valueOf(response.getResponseStatus()));
}
}
/**
* @Author haien
* @Description shiro的登录方法
* @Date 2019/3/26
* @Param [subject, request]
* @return boolean
**/
private boolean login(Subject subject,HttpServletRequest request){
//不处理get请求
if("get".equalsIgnoreCase(request.getMethod()))
return false;
String username=request.getParameter("username");
String password=request.getParameter("password");
if(StringUtils.isEmpty(username)||StringUtils.isEmpty(password))
return false;
UsernamePasswordToken token=new UsernamePasswordToken(username,password);
try {
subject.login(token);
return true;
} catch(Exception e){
request.setAttribute("error","登录失败:"+e.getClass().getName());
return false;
}
}
}
- 其中242行catch块无redirectUri时,返回原页面并打印错误信息如下:
- AccessTokenController:令牌控制器,负责用code获取token的过程。
- 首先访问该类方法:/accesstoken,并post数据:client_id=xxx & client_secret=xxx & grant_type=authorization_code & code=xxx & redirect_uri=http://localhost:9080/chapter17-client/oauth2-login,请求获取token。
- 控制器验证client_id、client_secret和code是否正确,否则生成错误响应;
- 正确则生成Access Token。
- 重定向回去,并带上Access Token。
@Controller
public class AccessTokenController {
@Resource
private OAuthService oAuthService;
@Resource
private UserService userService;
@RequestMapping("/accessToken")
public HttpEntity token(HttpServletRequest request)
throws OAuthSystemException {
try {
//1. 把当前请求封装成OAuth的请求
OAuthTokenRequest oAuthTokenRequest=new OAuthTokenRequest(request);
//2. 检查client_id、客户端key(client_secret)、许可证类型是否正确
if(!oAuthService.checkClientId(oAuthTokenRequest.getClientId())){
OAuthResponse response=OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.setError(OAuthError.TokenResponse.INVALID_CLIENT)
.setErrorDescription(
Constants.INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return new ResponseEntity(response.getBody(),
HttpStatus.valueOf(response.getResponseStatus()));
}
//客户端key
if(!oAuthService.checkClientSecret(
oAuthTokenRequest.getClientSecret())){
OAuthResponse response=OAuthASResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setError(OAuthError.TokenResponse.UNAUTHORIZED_CLIENT)
.setErrorDescription(
Constants.INVALID_CLIENT_DESCRIPTION)
.buildJSONMessage();
return new ResponseEntity(response.getBody(),
HttpStatus.valueOf(response.getResponseStatus()));
}
//许可证类型,此处只支持code类型,其他还有password和refresh_token
String authCode=oAuthTokenRequest.getParam(OAuth.OAUTH_CODE);
//从request中获取grant_type参数,判断是不是code类型
if(oAuthTokenRequest.getParam(OAuth.OAUTH_GRANT_TYPE).equals(
GrantType.AUTHORIZATION_CODE.toString())){
//检查code是否正确
if(!oAuthService.checkAuthCode(authCode)){
//生成错误响应
OAuthResponse response=OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.setError(OAuthError.TokenResponse.INVALID_GRANT)
.setErrorDescription("错误的授权码")
.buildJSONMessage();
return new ResponseEntity(response.getBody(),
HttpStatus.valueOf(response.getResponseStatus()));
}
}
//3. 生成Access Token
OAuthIssuer oauthissuer=new OAuthIssuerImpl(new MD5Generator());
final String accessToken=oauthissuer.accessToken(); //生成token
//加入缓存
oAuthService.addAccessToken(accessToken,
oAuthService.getUsernameByAuthCode(authCode));
//4. 重定向回去(不知道为什么这里没有把请求中的redirect_uri放进去)
//不需要return,自然会作为response带回去
OAuthResponse response=OAuthASResponse
.tokenResponse(HttpServletResponse.SC_OK)
.setAccessToken(accessToken)
//Long转String
.setExpiresIn(String.valueOf(oAuthService.getExpireIn()))
.buildJSONMessage();
return new ResponseEntity(response.getBody(),
HttpStatus.valueOf(response.getResponseStatus()))l
} catch (OAuthProblemException e) {
//生成错误响应
OAuthResponse response=OAuthASResponse
.errorResponse(HttpServletResponse.SC_BAD_REQUEST)
.error(e)
.buildJSONMessage();
return new ResponseEntity(response.getBody(),
HttpStatus.valueOf(response.getResponseStatus()));
}
}
}
资源服务器
- ResourceController:充当资源服务器,拥有user信息,根据token给客户端提供这些信息。
- 首先访问该类方法:/userInfo?access_token=xxx,即用token请求用户信息。
- 控制器判断token是否有效(存在并未过期),否则生成错误响应,客户端重新请求授权;
- 是则返回用户信息(此处为用户名)。
@RestController
public class ResourceController {
@Resource
private OAuthService oAuthService;
@RequestMapping("/userInfo")
public HttpEntity userInfo(HttpServletRequest request)
throws OAuthSystemException {
try {
//1. 将request包装成OAuth请求
OAuthAccessResourceRequest oAuthAccessResourceRequest=
new OAuthAccessResourceRequest(request);
//2. 检查Access Token是否正确
String accessToken=oAuthAccessResourceRequest.getAccessToken();
if(!oAuthService.checkAccessToken(accessToken)){ //不存再或已过期
//生成错误响应
OAuthResponse response=OAuthASResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setRealm(Constants.RESOURCE_SERVER_NAME)
.setError(OAuthError.ResourceResponse.INVALID_TOKEN)
.buildHeaderMessage();
HttpHeaders headers=new HttpHeaders();
headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,
response.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
return new ResponseEntity(headers,HttpStatus.UNAUTHORIZED);
}
//3. 返回资源(这里是用户名)
String username=oAuthService.getUsernameByAccessToken(accessToken);
return new ResponseEntity(username,HttpStatus.OK);
} catch (OAuthProblemException e) {
//是否设置了错误码
String errorCode=e.getError();
//没有错误码则不加错误码
if(OAuthUtils.isEmpty(errorCode)){
OAuthResponse response=OAuthASResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setRealm(Constants.RESOURCE_SERVER_NAME)
.buildHeaderMessage();
HttpHeaders headers=new HttpHeaders();
headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,
response.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
return new ResponseEntity(headers,HttpStatus.UNAUTHORIZED);
}
//有就加上
OAuthResponse response=OAuthASResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.setRealm(Constants.RESOURCE_SERVER_NAME)
.setError(e.getError())
.setErrorDescription(e.getDescription())
.setErrorUri(e.getUri())
.buildHeaderMessage();
HttpHeaders headers=new HttpHeaders();
headers.add(OAuth.HeaderType.WWW_AUTHENTICATE,
response.getHeader(OAuth.HeaderType.WWW_AUTHENTICATE));
return new ResponseEntity(HttpStatus.BAD_REQUEST);
}
}
}
配置文件:跟第十六章类似,其中要说的是过滤链的配置:OAuth的几个地址:/authorize、/accesstoken、/userInfo都是匿名可访问的。
/ = anon /login = authc /logout = logout /authorize=anon /accessToken=anon /userInfo=anon /** = user 测试:
- 首先模拟豆瓣向qq发起授权请求:http://localhost:8080/chapter17-server/authorize
?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee & response_type=code & redirect_uri=http://localhost:9080/chapter17-client/oauth2-login。 - 转发至登录页面,url仍是上面那个。
- 登录,提交至以上url进入login()验证,验证成功则重定向至redirect_uri(此时访问失败,因为还没有设置该url)并带上code。
- 重定向失败了先不管,先拿着code拿token:访问http://localhost:8080/chapter17-server/accessToken,post带上参数:client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee & client_secret=d8346ea2-6017-43ed-ad68-19c0f971738b & code=3569b277920280b33d6d86ded9022df8 & grant_type=authorization_code。返回token(json格式)。
- 带上token去请求用户信息:http://localhost:8080/chapter17-server/authorize
?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee & response_type=code & redirect_uri=http://localhost:9080/chapter17-client/oauth2-login,返回用户名。
客户端
- 流程:豆瓣客户端在登录时选择请求QQ服务器端授权登录,成功后返回code给客户端,客户端使用该code去服务器端换取token。
依赖:
org.apache.oltu.oauth2 org.apache.oltu.oauth2.client 0.31 OAuth2Token类:用于存储OAuth2服务端返回的auth code;待会被login方法调用;实现了AuthenticationToken接口,它用于保存用户提交的信息,常用的实现类是UsernamePasswordToken。
public class OAuth2Token implements AuthenticationToken {
private String authCode; //code
private String principal; //username
//构造方法
public OAuth2Token(String authCode) {
this.authCode = authCode;
}
//getter
@Override
public String getPrincipal() {
return principal;
}
@Override
public Object getCredentials() {
return authCode;
}
//setter
......
}
- OAuth2AuthenticationFilter: 过滤器,类似于FormAuthenticationFilter,控制OAuth2客户端的身份验证。
- 首先判断响应中是否包含error参数(除非重定向才会有响应),有则直接重定向到失败页面。
- 如果用户未身份验证且没有code,(若有code则是服务端授权之后返回的),则重定向到服务端进行授权;
- 如果未验证但有code,则调用executeLogin()进行登录(虽然服务端那边已经登录成功并拿到了code,但是客户端这边的Subject仍未登录,按第六章的说法,默认只有同线程的Subject是同一个,这里连应用都不是同一个),用code创建OAuth2Token提交给Subject登录。
- 登录成功回调onLoginSuccess()重定向到成功页面;
- 失败回调onLoginFailure()重定向到失败页面。
public class OAuth2AuthenticationFilter extends AuthenticatingFilter {
//auth code参数名
private String authcCodeParam="code";
private String clientId;
//服务器端登录成功后重定向地址
private String redirectUrl;
//失败后重定向地址
private String failureUrl;
private String responseType="code";
/**
* @Author haien
* @Description 用code创建OAuth2Token
* @Date 2019/3/30
* @Param [request, response]
* @return org.apache.shiro.authc.AuthenticationToken
**/
@Override
protected AuthenticationToken createToken(ServletRequest request,
ServletResponse response) throws Exception{
HttpServletRequest httpRequest=(HttpServletRequest)request;
String code=httpRequest.getParameter(authcCodeParam);
return new OAuth2Token(code);
}
@Override
protected boolean isAccessAllowed(ServletRequest request,
ServletResponse response, Object mappedValue) {
return false;
}
/**
* @Author haien
* @Description 当访问拒绝时是否已经处理了,返回true表示需要继续处理,
否则直接返回即可。不知道什么情况下会被拒绝?
* @Date 2019/3/30
* @Param [request, response]
* @return boolean
**/
@Override
protected boolean onAccessDenied(ServletRequest request,
ServletResponse response) throws Exception {
String error=request.getParameter("error");
String errorDescription=request.getParameter("error_description");
//如果服务端返回了错误参数
if(!StringUtils.isEmpty(error)){
//重定向到失败页面
WebUtils.issueRedirect(request,response,
failureUrl+"?error="+error+"error_description="+errorDescription);
return false;
}
//判断用户是否已身份验证(记住我不算)
Subject subject=getSubject(request,response);
if(!subject.isAuthenticated()){
//如果用户未身份验证且没有code
if(StringUtils.isEmpty(request.getParameter(authcCodeParam))){
//重定向到服务端授权
saveRequestAndRedirectToLogin(request,response);
return false;
}
/*未身份验证但已经重定向到server端登录页面并成功登录拿到code
(由于拿到code后OAuthresponse.buildQueryMessage()重定向回/oauth2-login
又触发该过滤器,因为server端和client端并不共享一个Subject,所以那边登录后
这边仍未登录),则执行父类的登录逻辑,它会调用createToken()用code创建
OAuth2Token并交给Subject.login()登录,login()将调用OAuth2Realm进行身份
验证;登录成功将回调onLoginSuccess()重定向到成功页面;
失败则onLoginFailure()重定向到失败页面*/
return executeLogin(request,response);
}
return false;
}
/**
* @Author haien
* @Description 登录成功后的回调方法,重定向到成功页面;
被上面executeLogin()调用
* @Date 2019/3/30
* @Param [token, subject, request, response]
* @return boolean
**/
@Override
protected boolean onLoginSuccess(AuthenticationToken token, Subject subject,
ServletRequest request,
ServletResponse response) throws Exception {
//把原先的url:/oauth2-login清空,跳转默认成功页面,不然又会触发过滤器,
//而执行onAccessdeny(),这次直接返回false,
//而给前端呈现了一个空白的/oauth2-login页面
WebUtils.getAndClearSavedRequest(request);
issueSuccessRedirect(request,response);
return false;
}
/**
* @Author haien
* @Description 登录失败后的回调;被上面executeLogin()调用
* @Date 2019/3/30
* @Param [token, e, request, response]
* @return boolean
**/
@Override
protected boolean onLoginFailure(AuthenticationToken token,
AuthenticationException ae,ServletRequest request,
ServletResponse response) {
Subject subject=getSubject(request,response);
//如果用户其实已登录过了
if(subject.isAuthenticated()||subject.isRemembered()){
//重定向到成功页面
try {
issueSuccessRedirect(request,response);
} catch (Exception e) {
e.printStackTrace();
}
}
//如果用户未登录且目前登录失败
else {
try {
//重定向到失败页面
WebUtils.issueRedirect(request,response,failureUrl);
} catch (IOException e) {
e.printStackTrace();
}
}
return false;
}
}
ShiroFilter:
/ = anon /oauth2Failure.jsp = anon /oauth2-login = oauth2Authc /logout = logout /** = user - loginUrl会自动设置到所有的AccessControllerFilter,包括OAuth2AuthenticationFilter。
/oauth2-login为服务端OAuth2授权起点,指定oauth2Authc过滤器。
测试:
- 访问http://localhost:9080/chapter17-client/,展示index.jsp登录页面,点击登录按钮,其url为/oauth2-login。
- 第一次触发OAuth2AuthenticationFilter,主要逻辑在onAccessDeny()中,它发现用户未登录,重定向到登录接口,按ShiroFilter设置的loginUrl,为http://localhost:8080/chapter17-server/authorize?带各参数,其中redirectUrl为/oauth2-login。
- 填写登录信息,提交server端AuthorizeController登录并带着code重定向到以上redirectUrl:/oauth2-login。
- 第二次又触发OAuth2AuthenticationFilter,在客户端执行login()登录一遍,其会调用OAuth2Realm中的extractUsername()拿着code去请求token进而请求userInfo获得username,封装进token,构造客户端的Subject;
- 登录成功,执行onLoginSuccess(),情况原本要访问的url:/oauth2-login,重定向到默认成功页面。
- 代码示例:ideaProjects/shiro-cahpter17
- 《跟我学shiro第十七章》