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

    在本项目中涉及到的类:

     

  • 相关阅读:
    September 29th 2017 Week 39th Friday
    September 28th 2017 Week 39th Thursday
    September 27th 2017 Week 39th Wednesday
    September 26th 2017 Week 39th Tuesday
    September 25th 2017 Week 39th Monday
    September 24th 2017 Week 39th Sunday
    angular2 学习笔记 ( Form 表单 )
    angular2 学习笔记 ( Component 组件)
    angular2 学习笔记 ( Http 请求)
    angular2 学习笔记 ( Router 路由 )
  • 原文地址:https://www.cnblogs.com/hhx626/p/7534565.html
Copyright © 2011-2022 走看看