OAuth2.0 授权码模式 实践
本篇文章不适合作为授权码模式的入门文章来阅读,适合想要自己实现授权码模式或体验授权码模式的开发者阅读
依赖知识
术语
- 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<String, String> params = uriInfo.getQueryParameters();
String state = params.getFirst("state");
if (state.length() > 0) {
stateMap.put(1, state);
}
String clientId = params.getFirst("client_id");
if (clientId == null || clientId.isEmpty()) {
return informUserAboutError(request, response, "Invalid client_id :" + clientId);
}
Client client = appDataRepository.getClient(clientId);
if (client == null) {
return informUserAboutError(request, response, "Invalid client_id :" + clientId);
}
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);
}
String redirectUri = params.getFirst("redirect_uri");
if (client.getRedirectUrl() != null && !client.getRedirectUrl().isEmpty()) {
if (redirectUri != null && redirectUri.isEmpty() && !client.getRedirectUrl().equals(redirectUri)) {
return informUserAboutError(request, response, "redirect_uri is pre-registred and should match");
}
redirectUri = client.getRedirectUrl();
} 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);
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:");
}
request.getSession().setAttribute("ORIGINAL_PARAMS", params);
String requestedScope = request.getParameter("scope");
if (requestedScope == null || requestedScope.isEmpty()) {
requestedScope = client.getScopes();
}
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<String, String> params) throws ServletException, IOException {
MultivaluedMap<String, String> originalParams = (MultivaluedMap<String, String>) request.getSession().getAttribute("ORIGINAL_PARAMS");
if (originalParams == null) {
informUserAboutError(request, response, "No pending authorization request.");
}
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();
}
List<String> 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<String, String> 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);
}
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<String, String> params) throws Exception {
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");
if (authorizationCode.getRedirectUrl() != null && !authorizationCode.getRedirectUrl().equals(redirectUri)) {
throw new WebApplicationException("invalid_grant");
}
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";
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");
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);
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;
@Override
@SneakyThrows
public void filter(ContainerRequestContext containerRequestContext) throws IOException {
Method method = resourceInfo.getResourceMethod();
String userId = "appuser";
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;
}
if (method.isAnnotationPresent(DenyAll.class)) {
log.info("no need to check permission");
return;
}
verifyTokenAndPermission(containerRequestContext, method);
}
@SneakyThrows
private void verifyTokenAndPermission(final ContainerRequestContext containerRequestContext, final Method method) {
MultivaluedMap<String, String> headers = containerRequestContext.getHeaders();
List<String> authorization = headers.get("Authorization");
String token = authorization.get(0).substring("Bearer".length()).trim();
JWSVerifier verifier = generateRsaJwsVerifier();
SignedJWT jwt = SignedJWT.parse(token);
if (!jwt.verify(verifier)) {
containerRequestContext.abortWith(buildResponse(Response.Status.FORBIDDEN));
}
Map<String, Object> claims = jwt.getJWTClaimsSet().getClaims();
String userId = jwt.getJWTClaimsSet().getSubject();
String scopes = (String) claims.get("scope");
log.info("scopes is:\n" + scopes);
List<String> parsedScopes = Arrays.asList(scopes.split("\\s+"));
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进行部署
项目完整代码
仓库地址
相关文章
|