java api 访问控制_java EE技术体系——CLF平台API开发注意事项(3)——API安全访问控制...

前言:提离职了,嗯,这么多年了,真到了提离职的时候,心情真的很复杂。好吧,离职阶段需要把一些项目中的情况说明白讲清楚,这篇博客就简单说一下在平台中对API所做的安全处理(后面讲网关还要说,这里主要讲代码结构)

一、宏观概况

第一点:系统是按照Security规范,通过实现OAuth2.0协议安全控制。

关键词理解:

安全协议:OAuth2,参考:理解OAuth 2.0

二、实现说明

2.1,安全访问过滤(重要)

在讲调用流程的时候,必须有必要说自定义的安全访问注解,云图平台的伙伴们,如果要理解系统的安全控制,或者仅是为了读接下来的流程说明,这一步很重要,一定要把这部分弄明白:  (这一段是JAX-RS规范很重要的内容)

首先看我们的自定义注解:

package com.dmsdbj.library.app.security;

import java.lang.annotation.ElementType;

import java.lang.annotation.Retention;

import java.lang.annotation.RetentionPolicy;

import java.lang.annotation.Target;

import javax.ws.rs.NameBinding;

@NameBinding

@Target({ElementType.TYPE, ElementType.METHOD})

@Retention(value = RetentionPolicy.RUNTIME)

public @interface Secured {

String[] value() default {};

}

注意里面的@NameBinding  ,请阅读:Per-JAX-RS

Method Bindings   必须要明白这个@NameBinding注解是用来干嘛的!!!

再看我们的过滤器:

@Priority(Priorities.AUTHENTICATION)

@Provider

@Secured

public class JWTAuthenticationFilter implements ContainerRequestFilter {

@Inject

private Logger log;

@Inject

private TokenProvider tokenProvider;

@Context

private HttpServletRequest request;

@Context

private ResourceInfo resourceInfo;

@Override

public void filter(ContainerRequestContext requestContext) throws IOException {

String jwt = resolveToken();

if (StringUtils.isNotBlank(jwt)) {

try {

if (tokenProvider.validateToken(jwt)) {

UserAuthenticationToken authenticationToken = this.tokenProvider.getAuthentication(jwt);

if (!isAllowed(authenticationToken)) {

requestContext.setProperty("auth-failed", true);

requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());

}

final SecurityContext securityContext = requestContext.getSecurityContext();

requestContext.setSecurityContext(new SecurityContext() {

@Override

public Principal getUserPrincipal() {

return authenticationToken::getPrincipal;

}

@Override

public boolean isUserInRole(String role) {

return securityContext.isUserInRole(role);

}

@Override

public boolean isSecure() {

return securityContext.isSecure();

}

@Override

public String getAuthenticationScheme() {

return securityContext.getAuthenticationScheme();

}

});

}

} catch (ExpiredJwtException eje) {

log.info("Security exception for user {} - {}", eje.getClaims().getSubject(), eje.getMessage());

requestContext.setProperty("auth-failed", true);

requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());

}

} else {

log.info("No JWT token found");

requestContext.setProperty("auth-failed", true);

requestContext.abortWith(Response.status(Response.Status.UNAUTHORIZED).build());

}

}

private String resolveToken() {

String bearerToken = request.getHeader(Constants.AUTHORIZATION_HEADER);

if (StringUtils.isNotEmpty(bearerToken) && bearerToken.startsWith("Bearer ")) {

String jwt = bearerToken.substring(7, bearerToken.length());

return jwt;

}

return null;

}

private boolean isAllowed(UserAuthenticationToken authenticationToken) {

Secured secured = resourceInfo.getResourceMethod().getAnnotation(Secured.class);

if (secured == null) {

secured = resourceInfo.getResourceClass().getAnnotation(Secured.class);

}

for (String role : secured.value()) {

if (!authenticationToken.getAuthorities().contains(role)) {

return false;

}

}

return true;

}

}附:1,You can bind a filter or interceptor to a particular annotation and when that custom annotation is applied, the filter or interceptor will automatically be bound to the annotated JAX-RS method.      (文章:Per-JAX-RS

Method Bindings )

2,By default, i.e. if no name binding is applied to the filter implementation class, the filter instance is applied globally, however only after the incoming request has been matched to a particular

resource by JAX-RS runtime. If there is a @NameBinding annotation applied to the filter, the filter will also be executed at the post-match request extension point, but only in case the matched resource or sub-resource method is bound to the same name-binding

annotation. (文章:CONTAINER REQUEST FILTER)

简单说来:这个本应该用于所有请求过滤的过滤器,因为加上了@Secure的注解(而@Secure注解又加上了@NameBinding注解),所以,这个过滤器仅被用于有@Secure修饰的特定类、方法!  备注:当前过滤器执行后匹配模式@Provider

2.2,正常访问流程

由上述的过滤器说明,要想请求经过安全限制的API(有@Seured修饰),必须要得到一个可用的token信息(resolveToken方法)。

所以,第一步通过登录获取票据:

服务端:

调用login方法(UserJWTController)

@Timed

@ApiOperation(value = "authenticate the credential")

@ApiResponses(value = {

@ApiResponse(code = 200, message = "OK")

,

@ApiResponse(code = 401, message = "Unauthorized")})

@Path("/authenticate")

@POST

@Consumes({MediaType.APPLICATION_JSON})

@Produces({MediaType.APPLICATION_JSON})

public Response login(@Valid LoginDTO loginDTO) throws ServletException {

UserAuthenticationToken authenticationToken = new UserAuthenticationToken(loginDTO.getUsername(), loginDTO.getPassword());

try {

User user = userService.authenticate(authenticationToken);

boolean rememberMe = (loginDTO.isRememberMe() == null) ? false : loginDTO.isRememberMe();

String jwt = tokenProvider.createToken(user, rememberMe);

return Response.ok(new JWTToken(jwt)).header(Constants.AUTHORIZATION_HEADER, "Bearer " + jwt).build();

} catch (AuthenticationException exception) {

return Response.status(Status.UNAUTHORIZED).header("AuthenticationException", exception.getLocalizedMessage()).build();

}

}

A:调用了userService.authenticate(authenticationToken),根据当前登录用户,查询用户信息及其角色信息;B:调用tokenProvider.createToken(user, rememberMe),为当前用户生成一个访问票据;C:将当前的票据信息存入到响应header。

客户端:

客户端接收到请求login方法后的Response,会从中提取票据token,并存入localStorage。本系统的具体代码位置:qpp/services/quth/auth.jwt.service  附:HTML

5 Web 存储

API请求:

在第一次登录获取完票据后,后续的请求,当请求的API有自定义注解@Secured时,经过过滤器,首先解析JWT判断是否拥有访问权限,再判断是否允许访问!

附:关键类TokenProvider

package com.dmsdbj.library.app.security.jwt;

import com.dmsdbj.library.app.config.SecurityConfig;

import com.dmsdbj.library.app.security.UserAuthenticationToken;

import com.dmsdbj.library.entity.User;

import java.util.*;

import java.util.stream.Collectors;

import javax.annotation.PostConstruct;

import javax.inject.Inject;

import org.slf4j.Logger;

import io.jsonwebtoken.*;

public class TokenProvider {

@Inject

private Logger log;

private static final String AUTHORITIES_KEY = "auth";

private String secretKey;

private long tokenValidityInSeconds;

private long tokenValidityInSecondsForRememberMe;

@Inject

private SecurityConfig securityConfig;

@PostConstruct

public void init() {

this.secretKey

= securityConfig.getSecret();

this.tokenValidityInSeconds

= 1000 * securityConfig.getTokenValidityInSeconds();

this.tokenValidityInSecondsForRememberMe

= 1000 * securityConfig.getTokenValidityInSecondsForRememberMe();

}

public String createToken(User user, Boolean rememberMe) {

String authorities = user.getAuthorities().stream()

.map(authority -> authority.getName())

.collect(Collectors.joining(","));

long now = (new Date()).getTime();

Date validity;

if (rememberMe) {

validity = new Date(now + this.tokenValidityInSecondsForRememberMe);

} else {

validity = new Date(now + this.tokenValidityInSeconds);

}

return Jwts.builder()

.setSubject(user.getLogin())

.claim(AUTHORITIES_KEY, authorities)

.signWith(SignatureAlgorithm.HS512, secretKey)

.setExpiration(validity)

.compact();

}

public UserAuthenticationToken getAuthentication(String token) {

Claims claims = Jwts.parser()

.setSigningKey(secretKey)

.parseClaimsJws(token)

.getBody();

Set authorities

= Arrays.asList(claims.get(AUTHORITIES_KEY).toString().split(",")).stream()

.collect(Collectors.toSet());

return new UserAuthenticationToken(claims.getSubject(), "", authorities);

}

public boolean validateToken(String authToken) {

try {

Jwts.parser().setSigningKey(secretKey).parseClaimsJws(authToken);

return true;

} catch (SignatureException e) {

log.info("Invalid JWT signature: " + e.getMessage());

return false;

}

}

}

三、总结

关于本平台的基本安全访问控制,大概就这些内容。其实挺简单的,就是模拟了一个票据生成中心,然后使用了JWT省去了读取服务器端session的步骤,仅通过解析JWT票据进行授权。    嗯,尽可能的在说明白,如果还是不明白的话,小伙伴们及时找我交流(先做任务,不然扛把子该......)

在本项目中涉及到的类:

681e5ac37a8ccfa74b35eae5f9208f37.png 

1954fbf0bb31d879139153f298b7e0af.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值