自定义实现OAuth2.0 授权码模式

自定义实现OAuth2.0 授权码模式,第1张

自定义实现OAuth2.0 授权码模式

文章目录
  • OAuth2.0 授权码模式 实践
    • 依赖知识
    • 术语
    • 授权码流程
    • 认证服务器
      • 拉起请求用户授权页面
      • 用户手动授权
      • 提交授权、生成code
      • 下发Token
    • 第三方应用
      • 收到code并请求Token
    • 访问受保护的资源
    • 项目结构
    • 项目部署
    • 项目完整代码
    • 相关文章

OAuth2.0 授权码模式 实践

本篇文章不适合作为授权码模式的入门文章来阅读,适合想要自己实现授权码模式或体验授权码模式的开发者阅读

依赖知识
  • RestEasy
  • nimbus-jose-jwt
  • JPA
  • CDI
  • javax.security
术语
  • Resource Owner-用户
  • Resource Server-负责处理对用户资源的请求
  • Authorization Server-获取用户的授权
  • Scope-对用户资源可 *** 作的范围
  • Client-第三方应用
授权码流程
  1. Resource Server提供Resource,Resource Owner具备对这些Resource的访问权限。

    注意 :请不要简单的把Resource想象成用户的信息、头像。因为在RESTful架构中,每一个URI代表一种Resource,理解这一点至关重要

  2. 当第三方应用Client想要访问Resource Owner的Resource时,需要经过Authorization Server的许可,才可以访问Resource Server上有关Resource Owner的Resource

详细流程如下:

  • 当Client想要访问Resource Owner 的Resource时,请求Authorization Server。response_type为code,代表为授权码流程,redirect_uri为用户授权后回调Client的url。

    Authorization Server接收到请求后,返回授权范围Scope,并拉起用户授权页面,请求Resource Owner授权

    https://authorization-server.com/auth
     ?response_type=code
     &client_id=29352915982374239857
     &redirect_uri=https://thirdparty-server/callback
     &state=xcoiv98y2kd22vusuye3kch
    
  • Resource Owner允许授权,并将授权范围Scope一并提交请求到Authorization Server。Authorization Server生成授权码,并设置过期时间,并存储在Authorization Server上。同时回调上一步请求中的redirect_uri对应的地址

    https://thirdparty-server/callback
     ?code=g0ZGZmNjVmOWIjNTk2NTk4ZTYyZGI3
     &state=xcoiv98y2kd22vusuye3kch
    
  • 第三方应用Client收到请求,比较state是否一致(防止CSRF攻击)。如果一致,则再次向Authorization Server发起请求,通过code换取access_token。

    grant_type=authorization_code
    code=g0ZGZmNjVmOWIjNTk2NTk4ZTYyZGI3
    client_id=client_id
    client_secret=client_secret
    
认证服务器

主要职责是请求Resource Owner授权和下发token

拉起请求用户授权页面
    @GET
    @DenyAll
    public Response applyForUserAuthorization(@Context HttpServletRequest request,
                                      @Context HttpServletResponse response,
                                      @Context UriInfo uriInfo) throws ServletException, IOException {
        MultivaluedMap params = uriInfo.getQueryParameters();
        String state = params.getFirst("state");
        // TODO: 实际业务中使用其他方式存储
        if (state.length() > 0) {
            stateMap.put(1, state);
        }
        //1. client_id
        String clientId = params.getFirst("client_id");
        if (clientId == null || clientId.isEmpty()) {
            return informUseraboutError(request, response, "Invalid client_id :" + clientId);
        }
        // 判断clientId是否在认证服务器中
        Client client = appDataRepository.getClient(clientId);
        if (client == null) {
            return informUseraboutError(request, response, "Invalid client_id :" + clientId);
        }
        //2. Client Authorized Grant Type
        if (client.getAuthorizedGrantTypes() != null && !client.getAuthorizedGrantTypes().contains("authorization_code")) {
            return informUseraboutError(request, response, "Authorization Grant type, authorization_code, is not allowed for this client :" + clientId);
        }
        //3. redirectUri
        String redirectUri = params.getFirst("redirect_uri");
        if (client.getRedirectUrl() != null && !client.getRedirectUrl().isEmpty()) {
            if (redirectUri != null && redirectUri.isEmpty() && !client.getRedirectUrl().equals(redirectUri)) {
                //sould be in the client.redirectUri
                return informUseraboutError(request, response, "redirect_uri is pre-registred and should match");
            }
            redirectUri = client.getRedirectUrl();
//            params.putSingle("resolved_redirect_uri", redirectUri);
        } else {
            if (redirectUri == null || redirectUri.isEmpty()) {
                return informUseraboutError(request, response, "redirect_uri is not pre-registred and should be provided");
            }
            params.putSingle("resolved_redirect_uri", redirectUri);
        }
        request.setAttribute("client", client);

        //4. response_type
        String responseType = params.getFirst("response_type");
        if (!"code".equals(responseType) && !"token".equals(responseType)) {
            return informUseraboutError(request, response, "invalid_grant :" + responseType + ", response_type params should be code or token:");
        }
        //Save params in session
        request.getSession().setAttribute("ORIGINAL_PARAMS", params);

        //4.scope: Optional
        String requestedScope = request.getParameter("scope");
        if (requestedScope == null || requestedScope.isEmpty()) {
            requestedScope = client.getScopes();
        }
        //5. user principal, common userId
        Principal principal = securityContext.getUserPrincipal();
        User user = appDataRepository.getUser(principal.getName());
        String allowedScopes = checkUserScopes(user.getScopes(), requestedScope);
        request.setAttribute("scopes", allowedScopes);
        // 转发至授权页面
        request.getRequestDispatcher("/authorize.jsp").forward(request, response);
        return null;
    }
用户手动授权

提交授权、生成code
    @DenyAll
    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    public void userAuthorization(@Context HttpServletRequest request,
                                      @Context HttpServletResponse response,
                                      MultivaluedMap params) throws ServletException, IOException {
        MultivaluedMap originalParams = (MultivaluedMap) request.getSession().getAttribute("ORIGINAL_PARAMS");
        if (originalParams == null) {
             informUseraboutError(request, response, "No pending authorization request.");
        }
//        String redirectUri = originalParams.getFirst("resolved_redirect_uri");
        String redirectUri = "http://localhost:8080/thirdparty-server/third/apply/callback";
        StringBuilder sb = new StringBuilder(redirectUri);
        sb.append("?state=").append(stateMap.get(1));
        String approvalStatus = params.getFirst("approval_status");
        if ("NO".equals(approvalStatus)) {
            URI location = UriBuilder.fromUri(sb.toString())
                    .queryParam("error", "User doesn't approved the request.")
                    .queryParam("error_description", "User doesn't approved the request.")
                    .build();
             Response.seeOther(location).build();
        }

        //==> YES
        List approvedScopes = params.get("scope");
        if (approvedScopes == null || approvedScopes.isEmpty()) {
            URI location = UriBuilder.fromUri(sb.toString())
                    .queryParam("error", "User doesn't approved the request.")
                    .queryParam("error_description", "User doesn't approved the request.")
                    .build();
             Response.seeOther(location).build();
        }
        String responseType = originalParams.getFirst("response_type");
        String clientId = originalParams.getFirst("client_id");
        if ("code".equals(responseType)) {
            String userId = securityContext.getUserPrincipal().getName();
            AuthorizationCode authorizationCode = new AuthorizationCode();
            authorizationCode.setCode(RandomString.make(15));
            authorizationCode.setClientId(clientId);
            authorizationCode.setUserId(userId);
            authorizationCode.setApprovedScopes(String.join(" ", approvedScopes));
            authorizationCode.setExpirationDate(LocalDateTime.now().plusMinutes(10));
            authorizationCode.setRedirectUrl(redirectUri);
            appDataRepository.save(authorizationCode);
            String code = authorizationCode.getCode();
            sb.append("&code=").append(code);
        }
        // 回调第三方应用
        response.sendRedirect(sb.toString());
    }
下发Token
    @POST
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces(MediaType.APPLICATION_JSON)
    @DenyAll
    public Response token(@HeaderParam(HttpHeaders.AUTHORIZATION) String authHeader,
                          MultivaluedMap params) {
        //Check grant_type params
        String grantType = params.getFirst("grant_type");
        if (grantType == null || grantType.isEmpty()) {
            return responseError("Invalid_request", "grant_type is required", Response.Status.BAD_REQUEST);
        }
        if (!supportedGrantTypes.contains(grantType)) {
            return responseError("unsupported_grant_type", "grant_type should be one of :" + supportedGrantTypes, Response.Status.BAD_REQUEST);
        }

        //Client Authentication
        String[] clientCredentials = extract(authHeader);
        if (clientCredentials.length != 2) {
            return responseError("Invalid_request", "Bad Credentials client_id/client_secret", Response.Status.BAD_REQUEST);
        }
        String clientId = clientCredentials[0];
        Client client = appDataRepository.getClient(clientId);
        if (client == null) {
            return responseError("Invalid_request", "Invalid client_id", Response.Status.BAD_REQUEST);
        }
        String clientSecret = clientCredentials[1];
        if (!clientSecret.equals(client.getClientSecret())) {
            return responseError("Invalid_request", "Invalid client_secret", Response.Status.UNAUTHORIZED);
        }
        AuthorizationGrantTypeHandler authorizationGrantTypeHandler = authorizationGrantTypeHandlers.select(NamedLiteral.of(grantType)).get();
        TokenVO tokenResponse = null;
        try {
            tokenResponse = authorizationGrantTypeHandler.createAccessToken(clientId, params);
        }catch (Exception ex) {
            log.log(Level.WARNING, "acquire token failed", ex);
        }
        return Response.ok(tokenResponse)
                .header("Cache-Control", "no-store")
                .header("Pragma", "no-cache")
                .build();
    }

这里只给出生成access_token的简单代码

@Named("authorization_code")
public class AuthorizationCodeGrantTypeHandler extends AbstractGrantTypeHandler{

    private EntityManager entityManager = JPAUtil.acquireEntityManager();

    @Inject
    private AppDataRepository appDataRepository;

    @Override
    public TokenVO createAccessToken(String clientId, MultivaluedMap params) throws Exception {
        //1. code is required
        String code = params.getFirst("code");
        if (code == null || "".equals(code)) {
            throw new WebApplicationException("invalid_grant");
        }
        AuthorizationCode authorizationCode = entityManager.find(AuthorizationCode.class, code);
        if (!authorizationCode.getExpirationDate().isAfter(LocalDateTime.now())) {
            throw new WebApplicationException("code Expired !");
        }
        String redirectUri = params.getFirst("redirect_uri");
        //redirecturi match
        if (authorizationCode.getRedirectUrl() != null && !authorizationCode.getRedirectUrl().equals(redirectUri)) {
            //redirectUri params should be the same as the requested redirectUri.
            throw new WebApplicationException("invalid_grant");
        }
        //client match
        if (!clientId.equals(authorizationCode.getClientId())) {
            throw new WebApplicationException("invalid_grant");
        }
        String accessToken = generateAccessToken(clientId, authorizationCode.getUserId(), authorizationCode.getApprovedScopes());
        String refreshToken = generateRefreshToken(clientId, authorizationCode.getUserId(), authorizationCode.getApprovedScopes());
        TokenVO result = new TokenVO();
        result.setAccess_token(accessToken);
        result.setExpires_in(expiresInMilliseconds);
        result.setScope(authorizationCode.getApprovedScopes());
        result.setRefresh_token(refreshToken);
        return result;
    }
}
第三方应用 收到code并请求Token
    @GET
    @Path("callback")
    @Produces(MediaType.APPLICATION_JSON)
    @SneakyThrows
    public Response callback(@Context HttpServletRequest request,
                             @Context HttpServletResponse response) {
    
        String clientId = "webappclient";
        String clientSecret = "webappclientsecret";
    
        //Error:
        String error = request.getParameter("error");
        if (error != null) {
            request.setAttribute("error", error);
            return Response.status(Status.INTERNAL_SERVER_ERROR).entity("获取access_token失败").build();
        }
        String localState = (String) request.getSession().getAttribute("CLIENT_LOCAL_STATE");
        if (!localState.equals(request.getParameter("state"))) {
            request.setAttribute("error", "The state attribute doesn't match !!");
            return Response.status(Status.INTERNAL_SERVER_ERROR).entity("校验state失败").build();
        }
    
        String code = request.getParameter("code");
        // 内部直接调用authorization-server获取token
        Client client = ClientBuilder.newClient();
        WebTarget target = client.target("http://localhost:8080/authorization-server/auth/token");
    
        Form form = new Form();
        form.param("grant_type", "authorization_code");
        form.param("code", code);
        form.param("redirect_uri", redirectUri);
        TokenVO tokenResponse = target.request(MediaType.APPLICATION_JSON_TYPE)
            .header(HttpHeaders.AUTHORIZATION, getAuthorizationHeaderValue(clientId, clientSecret))
            .post(Entity.entity(form, MediaType.APPLICATION_FORM_URLENCODED_TYPE), TokenVO.class);

        request.setAttribute("token", tokenResponse);
        // 将获取到的token显示在页面上
        request.getRequestDispatcher("/success.jsp").forward(request, response);
        return null;
    
    }
访问受保护的资源

利用刚才拿到的token,访问 Resource Server。Resource Server会根据token中的scope进行权限的校验,一般使用一个全局Filter来实现。通过对JWT的解析来判断该请求是否拥有对Resource访问的权限。

@Log
@Provider
public class SecurityFilter implements ContainerRequestFilter {

    @Context
    private ResourceInfo resourceInfo;


//    @Context
//    private HttpServletRequest httpServletRequest;

    @Override
    @SneakyThrows
    public void filter(ContainerRequestContext containerRequestContext) throws IOException {
        Method method = resourceInfo.getResourceMethod();
        //TODO: Question 还没找到解决方案,使用httpServletRequest的话,表单提交那里获取不到参数
//        String userId = httpServletRequest.getParameter("userId");
        String userId = "appuser";
        // 构造SecurityContext, 以便AuthorizationResource使用
        if (userId != null && userId.length() > 0) {
            containerRequestContext.setSecurityContext(new SecurityContext() {
                @Override
                public Principal getUserPrincipal() {
                    return () -> userId;
                }

                @Override
                public boolean isUserInRole(String role) {
                    return false;
                }

                @Override
                public boolean isSecure() {
                    return false;
                }

                @Override
                public String getAuthenticationScheme() {
                    return "CLIENT_CERT";
                }
            });
            return;
        }
         // no need to check permissions
        if (method.isAnnotationPresent(DenyAll.class)) {
            log.info("no need to check permission");
            return;
        }
        // 特殊情况都处理完毕之后,开始正常处理token的解析和权限的校验
        verifyTokenAndPermission(containerRequestContext, method);
    }

    @SneakyThrows
    private void verifyTokenAndPermission(final ContainerRequestContext containerRequestContext, final Method method) {
        MultivaluedMap headers = containerRequestContext.getHeaders();
        List authorization = headers.get("Authorization");
        String token = authorization.get(0).substring("Bearer".length()).trim();
        // verify token
        JWSVerifier verifier = generateRsaJwsVerifier();
        SignedJWT jwt = SignedJWT.parse(token);
        if (!jwt.verify(verifier)) {
            containerRequestContext.abortWith(buildResponse(Response.Status.FORBIDDEN));
        }
        Map claims = jwt.getJWTClaimsSet().getClaims();
        String userId = jwt.getJWTClaimsSet().getSubject();
        String scopes = (String) claims.get("scope");
        log.info("scopes is:n" + scopes);
        List parsedScopes = Arrays.asList(scopes.split("\s+"));


        // verify permission
        if (method.isAnnotationPresent(RolesAllowed.class)) {
            RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class);
            String[] roles = rolesAnnotation.value();
            if (!parsedScopes.containsAll(Arrays.asList(roles))) {
                containerRequestContext.abortWith(buildResponse(Response.Status.FORBIDDEN));
            }
            containerRequestContext.setSecurityContext(new SecurityContext() {
                @Override
                public Principal getUserPrincipal() {
                    return () -> userId;
                }

                @Override
                public boolean isUserInRole(String role) {
                    return parsedScopes.contains(role);
                }

                @Override
                public boolean isSecure() {
                    return true;
                }

                @Override
                public String getAuthenticationScheme() {
                    return "CLIENT_CERT";
                }
            });
        }
    }

    private JWSVerifier generateRsaJwsVerifier() throws Exception{
        String pemEncodedRSAPrivateKey = PEMKeyUtils.readKeyAsString("rsa/publish-key.pem");
        RSAKey rsaKey = (RSAKey) JWK.parseFromPEMEncodedObjects(pemEncodedRSAPrivateKey);
        return new RSASSAVerifier(rsaKey);
    }

    private Response buildResponse(Response.Status status) {
        return Response
                .status(status)
                .entity("{"errmsg": ""}")
                .type(MediaType.APPLICATION_JSON_TYPE)
                .build();
    }
}

真正访问Resource上的Resource

@Path("user/protect")
public class UserResource {

    @GET
    @Path("read")
    @Produces(MediaType.TEXT_PLAIN)
    @RolesAllowed("resource.read")
    public String readProtectedInfo() {
        return "Read Success";
    }

    @POST
    @Path("write")
    @Consumes(MediaType.APPLICATION_FORM_URLENCODED)
    @Produces(MediaType.TEXT_PLAIN)
    @RolesAllowed("resource.write")
    public String writeProtectedInfo(@FormParam("writeInfo") String writeInfo) {
        return "Write Success n" + writeInfo;
    }
}
项目结构

项目部署

先在authorization-common下执行

mvn install

再使用Tomcat9进行部署

项目完整代码

仓库地址

相关文章
  • oauth2快速入门
  • What is the OAuth 2.0 Authorization Code Grant Type?

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5671691.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-16
下一篇 2022-12-16

发表评论

登录后才能评论

评论列表(0条)

保存