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


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

    一、宏观概况

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

    关键词理解:

    JWT:JWTJWT 在前后端分离中的应用与实践

    规范:Security、JAX-RS(当前选取Jersey:Difference between JAX-RS, Restlet, Jersey, RESTEasy, and Apache CXF Frameworks

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

    其他:java自定义注解RBACCONTAINER REQUEST FILTER

    二、实现说明

    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<String> 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票据进行授权。    嗯,尽可能的在说明白,如果还是不明白的话,小伙伴们及时找我交流(先做任务,不然扛把子该......)

    在本项目中涉及到的类:

     

  • 相关阅读:
    @EnableTransactionManagement的使用
    Spring事务管理之几种方式实现事务
    instr和like的使用区别
    linux查看服务安装目录redis
    struts2的结果类型
    ajax 的 get 方式
    数据库隔离级别
    数据库隔离级别
    input from 表单提交 使用 属性 disabled=&quot;disabled&quot; 后台接收不到name=&quot;username&quot;的值
    Path Sum
  • 原文地址:https://www.cnblogs.com/hhx626/p/7534565.html
Copyright © 2020-2023  润新知