简介

  • 使用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<String>(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资源服务器两个依赖。

    <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>
    
授权服务器
  • 以下两个controller一个负责根据登录表单给code,一个负责根据code给token,合为授权服务器。

  • AuthorizeController:授权控制器,控制客户端访问OAth服务器端的授权登录页面,通过授权验证并获取code授权码。

  1. 首先访问该controller第一个方法地址:/authorize?client_id=xxx&response_type=code&redirect_uri=http://localhost:9080/chapter17-client/oauth2-login,即访问授权页面。
  2. 控制器首先检查clientId是否正确,如果错误则返回错误响应;
  3. 如果正确则判断用户是否已登录,否则跳转登录页面;
  4. 已登录则生成相应auth code,并保存到缓存中。
  5. 重定向回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的过程。
  1. 首先访问该类方法:/accesstoken,并post数据:client_id=xxx & client_secret=xxx & grant_type=authorization_code & code=xxx & redirect_uri=http://localhost:9080/chapter17-client/oauth2-login,请求获取token。
  2. 控制器验证client_id、client_secret和code是否正确,否则生成错误响应;
  3. 正确则生成Access Token。
  4. 重定向回去,并带上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给客户端提供这些信息。
  1. 首先访问该类方法:/userInfo?access_token=xxx,即用token请求用户信息。
  2. 控制器判断token是否有效(存在并未过期),否则生成错误响应,客户端重新请求授权;
  3. 是则返回用户信息(此处为用户名)。
@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都是匿名可访问的。

    <property name="filterChainDefinitions">
        <!--OAuth的几个地址:/authorize、/accesstoken、/userInfo都是匿名可访问的-->
        <value>
            / = anon
            /login = authc
            /logout = logout
    
            /authorize=anon
            /accessToken=anon
            /userInfo=anon
    
            /** = user
        </value>
    </property>
    
  • 测试:

  1. 首先模拟豆瓣向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。
  2. 转发至登录页面,url仍是上面那个。
  3. 登录,提交至以上url进入login()验证,验证成功则重定向至redirect_uri(此时访问失败,因为还没有设置该url)并带上code。
  4. 重定向失败了先不管,先拿着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格式)。
  5. 带上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。
  • 依赖:

    <dependency>
      <groupId>org.apache.oltu.oauth2</groupId>
      <artifactId>org.apache.oltu.oauth2.client</artifactId>
      <version>0.31</version>
    </dependency>
    
  • 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客户端的身份验证。
  1. 首先判断响应中是否包含error参数(除非重定向才会有响应),有则直接重定向到失败页面。
  2. 如果用户未身份验证且没有code,(若有code则是服务端授权之后返回的),则重定向到服务端进行授权;
  3. 如果未验证但有code,则调用executeLogin()进行登录(虽然服务端那边已经登录成功并拿到了code,但是客户端这边的Subject仍未登录,按第六章的说法,默认只有同线程的Subject是同一个,这里连应用都不是同一个),用code创建OAuth2Token提交给Subject登录。
  4. 登录成功回调onLoginSuccess()重定向到成功页面;
  5. 失败回调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:

    <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
      <property name="securityManager" ref="securityManager"/>
      <property name="loginUrl"
                value="http://localhost:8080/chapter17-server/authorize?client_id=c1ebe466-1cdc-4bd3-ab69-77c3561b9dee&amp;response_type=code&amp;redirect_uri=http://localhost:9080/chapter17-client/oauth2-login"/>
      <property name="successUrl" value="/"/>
      <property name="filters">
          <util:map>
             <entry key="oauth2Authc" value-ref="oAuth2AuthenticationFilter"/>
          </util:map>
      </property>
      <property name="filterChainDefinitions">
          <value>
              / = anon
              /oauth2Failure.jsp = anon
              /oauth2-login = oauth2Authc
              /logout = logout
              /** = user
          </value>
      </property>
    </bean>
    
  • loginUrl会自动设置到所有的AccessControllerFilter,包括OAuth2AuthenticationFilter。
  • /oauth2-login为服务端OAuth2授权起点,指定oauth2Authc过滤器。

  • 测试:

  1. 访问http://localhost:9080/chapter17-client/,展示index.jsp登录页面,点击登录按钮,其url为/oauth2-login。
  2. 第一次触发OAuth2AuthenticationFilter,主要逻辑在onAccessDeny()中,它发现用户未登录,重定向到登录接口,按ShiroFilter设置的loginUrl,为http://localhost:8080/chapter17-server/authorize?带各参数,其中redirectUrl为/oauth2-login。
  3. 填写登录信息,提交server端AuthorizeController登录并带着code重定向到以上redirectUrl:/oauth2-login。
  4. 第二次又触发OAuth2AuthenticationFilter,在客户端执行login()登录一遍,其会调用OAuth2Realm中的extractUsername()拿着code去请求token进而请求userInfo获得username,封装进token,构造客户端的Subject;
  5. 登录成功,执行onLoginSuccess(),情况原本要访问的url:/oauth2-login,重定向到默认成功页面。