- OAuth2.0 授权码模式 实践
- 依赖知识
- 术语
- 授权码流程
- 认证服务器
- 拉起请求用户授权页面
- 用户手动授权
- 提交授权、生成code
- 下发Token
- 第三方应用
- 收到code并请求Token
- 访问受保护的资源
- 项目结构
- 项目部署
- 项目完整代码
- 相关文章
本篇文章不适合作为授权码模式的入门文章来阅读,适合想要自己实现授权码模式或体验授权码模式的开发者阅读
依赖知识- RestEasy
- nimbus-jose-jwt
- JPA
- CDI
- javax.security
- Resource Owner-用户
- Resource Server-负责处理对用户资源的请求
- Authorization Server-获取用户的授权
- Scope-对用户资源可 *** 作的范围
- Client-第三方应用
-
Resource Server提供Resource,Resource Owner具备对这些Resource的访问权限。
注意 :请不要简单的把Resource想象成用户的信息、头像。因为在RESTful架构中,每一个URI代表一种Resource,理解这一点至关重要
-
当第三方应用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用户手动授权 提交授权、生成codeparams = 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; }
@DenyAll @POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) public void userAuthorization(@Context HttpServletRequest request, @Context HttpServletResponse response, MultivaluedMap下发Tokenparams) 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()); }
@POST @Consumes(MediaType.APPLICATION_FORM_URLENCODED) @Produces(MediaType.APPLICATION_JSON) @DenyAll public Response token(@HeaderParam(HttpHeaders.AUTHORIZATION) String authHeader, MultivaluedMapparams) { //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第三方应用 收到code并请求Tokenparams) 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; } }
@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) { MultivaluedMapheaders = 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?
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)