Spring Security+JWT RBAC動的認可の完了

114479 ワード

この記事はspring securityシリーズの最初の記事で、spring securityによってエンタープライズ・プロジェクトの権限制御を完了する方法と、RedisによってJWTの失効を制御する方法に重点を置いています.
1.RBACとは
RBAC(Role-Based Access Control)は、ロールの権限制御に基づいて、ロールに権限が関連付けられ、ユーザは、適切なロールのメンバーになることによって、これらのロールの権限を得る.
2.JWTとSpring Security
Spring securityライセンスは主に2つに分けられ、1つはsecurity内部でログインユーザーのメンテナンスを担当するセッションであり、1つはJWT方式を採用し、セッションを管理しない.JWTとSecurityの詳細については、仲間たちが自分で調べてください(関連サイトのお勧め:http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html)ここでは説明しませんので、本文を始めましょう.
3.1依存のインポート
<!-- spring security   jwt -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

3.securityコア構成クラス:WebSecurityConfig
@Configuration
@EnableWebSecurity
@EnableConfigurationProperties(CustomConfig.class)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomConfig customConfig;

    @Autowired
    private SecurityUserService securityUserService;

    @Autowired
    @Qualifier("securityAccessDeniedHandler")
    private AccessDeniedHandler securityAccessDeniedHandler;

    @Autowired
    private JwtAuthenticationFilter jwtAuthenticationFilter;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(securityUserService).passwordEncoder(passwordEncoder());
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http.cors()
                //    CSRF
                .and().csrf().disable()
                //          ,   LoginController#login
                .formLogin().disable()
                .httpBasic().disable()

                //     
                .authorizeRequests()
                //            
                .anyRequest()
                .authenticated()
                // RBAC    url   
                .anyRequest()
                .access("@rbacAuthorityService.hasPermission(request,authentication)")

                //          ,   LoginController#logout
                .and().logout().disable()
                //     
                .exceptionHandling().accessDeniedHandler(securityAccessDeniedHandler);

        // Session   
        http.sessionManagement()
                //      JWT,       Session
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS);

        //       JWT    
        http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }

    /**
     *                  ,   AuthController
     *      {@link #configure(HttpSecurity)}    
     * {@code http.authorizeRequests().antMatchers("/api/auth/**").permitAll()}
     */
    @Override
    public void configure(WebSecurity web) {
        WebSecurity and = web.ignoring().and();

        //    GET
        customConfig.getIgnores().getGet().forEach(url -> and.ignoring().antMatchers(HttpMethod.GET, url));

        //    POST
        customConfig.getIgnores().getPost().forEach(url -> and.ignoring().antMatchers(HttpMethod.POST, url));

        //    DELETE
        customConfig.getIgnores().getDelete().forEach(url -> and.ignoring().antMatchers(HttpMethod.DELETE, url));

        //    PUT
        customConfig.getIgnores().getPut().forEach(url -> and.ignoring().antMatchers(HttpMethod.PUT, url));

        //    HEAD
        customConfig.getIgnores().getHead().forEach(url -> and.ignoring().antMatchers(HttpMethod.HEAD, url));

        //    PATCH
        customConfig.getIgnores().getPatch().forEach(url -> and.ignoring().antMatchers(HttpMethod.PATCH, url));

        //    OPTIONS
        customConfig.getIgnores().getOptions().forEach(url -> and.ignoring().antMatchers(HttpMethod.OPTIONS, url));

        //    TRACE
        customConfig.getIgnores().getTrace().forEach(url -> and.ignoring().antMatchers(HttpMethod.TRACE, url));

        //         
        customConfig.getIgnores().getPattern().forEach(url -> and.ignoring().antMatchers(url));
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    /**
     *       
     * @return
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

1.AuthenticationManagerBuilder authは主にsecurityの暗号化方式を設置している(BCryptPasswordEncoderも現在比較的流行している安全な暗号化方式であり、MD 5よりも効率が高い)、userDetailsServiceはユーザー名、パスワードの検証と授権を担当している.
2.HttpSecurity httpは主にsecurityコアフィルタチェーンの配置であり、登録、登録、異常などのプロセッサを配置することができる.JWT方式を採用しているため、securityが提供する登録と登録を無効にし、JWTのフィルタを配置し、RBAC検査の方式を配置した.
3.WebSecurity webは主に、プロファイルに設定されたローのURLをcustomConfigで読み取るsecurityローのパスの構成を担当します.
4.JWTの認証フィルタJwtAuthenticationFilterの構成
/**
 * @author lirong
 * @ClassName: JwtAuthenticationFilter
 * @Description: Jwt      
 * @date 2019-07-12 9:50
 */
@Component
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private SecurityUserService userDetailsService;

    @Autowired
    private JwtUtil jwtUtil;

    @Autowired
    private CustomConfig customConfig;

    @Autowired
    private IApplicationConfig applicationConfig;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {

        //         
        if (checkIgnores(request)) {
            chain.doFilter(request, response);
            return;
        }

        String jwt = jwtUtil.getJwtFromRequest(request);

        if (StrUtil.isNotBlank(jwt)) {
            try {
                String username = jwtUtil.getUsernameFromJWT(jwt, false);

                UserDetails userDetails = userDetailsService.loadUserByUsername(username);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

                SecurityContextHolder.getContext().setAuthentication(authentication);

                chain.doFilter(request, response);
            } catch (CustomException e) {
                ResponseUtils.renderJson(request, response, e, applicationConfig.getOrigins());
            }
        } else {
            ResponseUtils.renderJson(request, response, ResultCode.UNAUTHORIZED, null, applicationConfig.getOrigins());
        }
    }

    /**
     *              
     * @param request     
     * @return true -   ,false -    
     */
    private boolean checkIgnores(HttpServletRequest request) {
        String method = request.getMethod();

        HttpMethod httpMethod = HttpMethod.resolve(method);
        if (ObjectUtil.isNull(httpMethod)) {
            httpMethod = HttpMethod.GET;
        }

        Set<String> ignores = Sets.newHashSet();

        switch (httpMethod) {
            case GET:
                ignores.addAll(customConfig.getIgnores()
                        .getGet());
                break;
            case PUT:
                ignores.addAll(customConfig.getIgnores()
                        .getPut());
                break;
            case HEAD:
                ignores.addAll(customConfig.getIgnores()
                        .getHead());
                break;
            case POST:
                ignores.addAll(customConfig.getIgnores()
                        .getPost());
                break;
            case PATCH:
                ignores.addAll(customConfig.getIgnores()
                        .getPatch());
                break;
            case TRACE:
                ignores.addAll(customConfig.getIgnores()
                        .getTrace());
                break;
            case DELETE:
                ignores.addAll(customConfig.getIgnores()
                        .getDelete());
                break;
            case OPTIONS:
                ignores.addAll(customConfig.getIgnores()
                        .getOptions());
                break;
            default:
                break;
        }

        ignores.addAll(customConfig.getIgnores()
                .getPattern());

        if (CollUtil.isNotEmpty(ignores)) {
            for (String ignore : ignores) {
                AntPathRequestMatcher matcher = new AntPathRequestMatcher(ignore, method);
                if (matcher.matches(request)) {
                    return true;
                }
            }
        }
        return false;
    }
}

このフィルタは、システムにアクセスするすべての要求をブロックするため、ログインとログインを含む無視されたすべてのURLを解放し、検証されたJWTのユーザー情報をauthenticationにカプセル化する必要があります.
5.RBAC権限照合器
/**
 * @author lirong
 * @ClassName: JwtAuthenticationFilter
 * @Description: Jwt      
 * @date 2019-07-12 9:50
 */
@Slf4j
@Component
public class RbacAuthorityService {
    @Autowired
    private SecurityUserService userDetails;

    @Autowired
    private RequestMappingHandlerMapping mapping;

    public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
        checkRequest(request);

        Object userInfo = authentication.getPrincipal();
        boolean hasPermission = false;

        if (userInfo instanceof UserDetails) {
            SecurityUser principal = (SecurityUser) userInfo;
            SecurityUser userDTO = (SecurityUser) this.userDetails.loadUserByUsername(principal.getUsername());

            //    ,     ,        ,       
            List<MenuRight> btnPerms = userDTO.getMenus().stream()
                    //       
                    .filter(menuRight -> menuRight.getGrades() >= 3)
                    //    URL   
                    .filter(menuRight -> StrUtil.isNotBlank(menuRight.getUrl()))
                    //    METHOD   
                    .collect(Collectors.toList());
            for (MenuRight btnPerm : btnPerms) {
                AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(btnPerm.getUrl(), btnPerm.getMethod());
                if (antPathMatcher.matches(request)) {
                    hasPermission = true;
                    break;
                }
            }

            return hasPermission;
        } else {
            return false;
        }
    }

    /**
     *         
     *
     * @param request   
     */
    private void checkRequest(HttpServletRequest request) {
        //      request    
        String currentMethod = request.getMethod();
        Multimap<String, String> urlMapping = allUrlMapping();

        for (String uri : urlMapping.keySet()) {
            //    AntPathRequestMatcher    url
            //      2       AntPathRequestMatcher
            // 1:new AntPathRequestMatcher(uri,method)                 ,                   ,  ,     2     
            // 2:new AntPathRequestMatcher(uri)            ,       
            AntPathRequestMatcher antPathMatcher = new AntPathRequestMatcher(uri);
            if (antPathMatcher.matches(request)) {
                if (!urlMapping.get(uri)
                        .contains(currentMethod)) {
                    throw new CustomException(ResultCode.HTTP_BAD_METHOD);
                } else {
                    return;
                }
            }
        }

        throw new CustomException(ResultCode.REQUEST_NOT_FOUND);
    }

    /**
     *      URL Mapping,     {"/test":["GET","POST"],"/sys":["GET","DELETE"]}
     *
     * @return {@link ArrayListMultimap}     URL Mapping
     */
    private Multimap<String, String> allUrlMapping() {
        Multimap<String, String> urlMapping = ArrayListMultimap.create();

        //   url          
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = mapping.getHandlerMethods();

        handlerMethods.forEach((k, v) -> {
            //      key       URL
            Set<String> url = k.getPatternsCondition()
                    .getPatterns();
            RequestMethodsRequestCondition method = k.getMethodsCondition();

            //    URL         
            url.forEach(s -> urlMapping.putAll(s, method.getMethods()
                    .stream()
                    .map(Enum::toString)
                    .collect(Collectors.toList())));
        });

        return urlMapping;
    }
}

この方法は多くのように見えますが、実際にはページリクエストのURLとユーザーが所有するすべての権限リソース(URL)を一致させることしかできません.
6.UserDetailsServiceデータベースユーザー情報の照会
/**
 * @author lirong
 * @Date 2019-7-14 22:46:54
 * @Desc           
 */
@Component("securityUserService")
public class SecurityUserService implements UserDetailsService {
    @Resource
    private UserMapper userMapper;
    @Resource
    private RoleMapper roleMapper;
    @Resource
    private MenuRightMapper menuRightMapper;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        UserDTO userDTO = userMapper.getRolesByUsername(username);
        //     ID 1     
        if (null != userDTO){
            if(1L == userDTO.getId()) {
                this.getAdminPermission(userDTO);
            }
            SecurityUser securityUser = new SecurityUser(LoginUserDTO.user2LoginUserDTO(userDTO));
            return securityUser;
        } else {
            throw new UsernameNotFoundException(username + "      !");
        }
    }

    /**
     *          
     * @param userDTO
     * @return
     */
    private UserDTO getAdminPermission(UserDTO userDTO) {
        List<Role> roles = roleMapper.selectAll();
        List<MenuRight> menuRights = menuRightMapper.selectAll();
        userDTO.setRoles(roles);
        userDTO.setMenus(menuRights);
        return userDTO;
    }
}

7.JWTのリフレッシュおよびログインユーザのログアウト
周知のように、JWTは無状態であり、サービス側はJWTを解析することによってユーザが事前にログアウトしたかどうかを知ることができないため、Redisの期限切れメカニズムを利用して、ユーザに終了を通知する目的を達成する.JWTが作成されると、生成されたJWTはユーザ名を接頭辞としてRedisに格納され、終了するとRedisのこのユーザ名のJWTをクリアし、アクセスするたびにJWTを解析し、Redisにこのユーザ名のJWTがまだ存在するかどうかを判断し、存在しない場合は、このユーザがログアウトしたことを示す.JWTの再署名です.ここではrefresh_を採用しています.tokenの形式と、ログイン時に2つのJWT、1つのtoken、1つのrefresh_を作成します.token,refresh_tokenの有効期限の設定は比較的に長く、tokenが無効になった後、フロントエンドはrefreshを呼び出すことができます.tokenのインタフェースをリフレッシュして、再署名の目的を達成します.
/**
 * jwt   
 * @author  daiyp
 * @date 2018-9-26
 */
@EnableConfigurationProperties(JwtConfig.class)
@Configuration
@Slf4j
public class JwtUtil {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private JwtConfig jwtConfig;

    /**
     *   JWT
     *
     * @param authentication       
     * @param rememberMe        
     * @return JWT
     */
    public String createJWT(Authentication authentication, Boolean rememberMe, Boolean isRefresh) {
        SecurityUser user = (SecurityUser) authentication.getPrincipal();
        return createJWT(isRefresh, rememberMe, user.getId(), user.getUsername(), user.getRoles(), user.getMenus(), user.getAuthorities());
    }

    /**
     *   JWT
     *
     * @param id            id
     * @param subject        
     * @param roles           
     * @param authorities     
     * @return JWT
     */
    public String createJWT(Boolean isRefresh,
                            Boolean rememberMe,
                            Long id,
                            String subject,
                            List<Role> roles,
                            List<MenuRight> menus,
                            Collection<? extends GrantedAuthority> authorities) {
        Date now = new Date();
        JwtBuilder builder = Jwts.builder()
                .setId(id.toString())
                .setSubject(subject)
                .setIssuedAt(now)
                .signWith(SignatureAlgorithm.HS256, jwtConfig.getSecret())
                .claim("roles", roles)
                // .claim("perms", menus)
                .claim("authorities", authorities);

        //       
        Long ttl = rememberMe ? jwtConfig.getRemember() : jwtConfig.getTtl();
        String redisKey;
        if (isRefresh){
            ttl *= 3;
            redisKey = Constant.REDIS_JWT_REFRESH_TOKEN_KEY_PREFIX + subject;
        }else{
            redisKey = Constant.REDIS_JWT_TOKEN_KEY_PREFIX + subject;
        }
        if (ttl > 0) {
            builder.setExpiration(DateUtil.offsetMillisecond(now, ttl.intValue()));
        }

        String jwt = builder.compact();
        //     JWT   Redis
        redisTemplate.opsForValue().set(redisKey, jwt, ttl, TimeUnit.MILLISECONDS);
        return jwt;
    }

    /**
     *   JWT
     *
     * @param jwt JWT
     * @return {@link Claims}
     */
    public Claims parseJWT(String jwt, Boolean isRefresh) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(jwtConfig.getSecret())
                    .parseClaimsJws(jwt)
                    .getBody();

            String username = claims.getSubject();
            String redisKey = (isRefresh ? Constant.REDIS_JWT_REFRESH_TOKEN_KEY_PREFIX : Constant.REDIS_JWT_TOKEN_KEY_PREFIX)
                    + username;

            //   redis  JWT    
            Long expire = redisTemplate.getExpire(redisKey, TimeUnit.MILLISECONDS);
            if (Objects.isNull(expire) || expire <= 0) {
                throw new CustomException(ResultCode.TOKEN_EXPIRED);
            }

            //   redis  JWT        ,           /         ,   JWT   
            String redisToken = (String) redisTemplate.opsForValue().get(redisKey);
            if (!StrUtil.equals(jwt, redisToken)) {
                throw new CustomException(ResultCode.TOKEN_OUT_OF_CTRL);
            }
            return claims;
        } catch (ExpiredJwtException e) {
            log.error("Token    ");
            throw new CustomException(ResultCode.TOKEN_EXPIRED);
        } catch (UnsupportedJwtException e) {
            log.error("     Token");
            throw new CustomException(ResultCode.TOKEN_PARSE_ERROR);
        } catch (MalformedJwtException e) {
            log.error("Token   ");
            throw new CustomException(ResultCode.TOKEN_PARSE_ERROR);
        } catch (IllegalArgumentException e) {
            log.error("Token      ");
            throw new CustomException(ResultCode.TOKEN_PARSE_ERROR);
        }
    }

    /**
     *   JWT  
     *
     * @param request   
     */
    public void invalidateJWT(HttpServletRequest request) {
        String jwt = getJwtFromRequest(request);
        String username = getUsernameFromJWT(jwt, false);
        //  redis   JWT
        redisTemplate.delete(Constant.REDIS_JWT_REFRESH_TOKEN_KEY_PREFIX + username);
        redisTemplate.delete(Constant.REDIS_JWT_TOKEN_KEY_PREFIX + username);
    }

    /**
     *   request   header     JWT
     *
     * @param request   
     * @return JWT
     */
    public String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

    /**
     *    jwt      
     *
     * @param jwt JWT
     * @return    
     */
    public String getUsernameFromJWT(String jwt, Boolean isRefresh) {
        Claims claims = parseJWT(jwt, isRefresh);
        return claims.getSubject();
    }

    public Map<String, String> refreshJWT(String token) {
        Claims claims = parseJWT(token, true);
        //       
        Date lastTime = claims.getExpiration();
        // 1.   refreshToken    
        if (!new Date().before(lastTime)){
            throw new CustomException(ResultCode.TOKEN_EXPIRED);
        }
        // 2.  redis      token refreshToken
        String username = claims.getSubject();
        // redisTemplate.delete(Constant.REDIS_JWT_REFRESH_TOKEN_KEY_PREFIX + username);
        // redisTemplate.delete(Constant.REDIS_JWT_TOKEN_KEY_PREFIX + username);
        // 3.     token refreshToken   redis
        String jwtToken = createJWT(false, false, Long.parseLong(claims.getId()), username,
                (List<Role>) claims.get("roles"), null, (Collection<? extends GrantedAuthority>) claims.get("authorities"));
        String refreshJwtToken = createJWT(true, false, Long.parseLong(claims.getId()), username,
                (List<Role>) claims.get("roles"), null, (Collection<? extends GrantedAuthority>) claims.get("authorities"));
        Map<String, String> map = new HashMap<>();
        map.put("token", jwtToken);
        map.put("refreshToken", refreshJwtToken);
        return map;
    }

    /**
     *
     *   :   jwt token
* @param name * @param param * @param secret * @param expirationtime (5 5*60*1000) * @return * */
public static String sign(String name, Map<String,Object> param, String secret, Long expirationtime){ String JWT = Jwts.builder() .setClaims(param) .setSubject(name) .setExpiration(new Date(System.currentTimeMillis() + expirationtime)) .signWith(SignatureAlgorithm.HS256,secret) .compact(); return JWT; } /** * * : jwt
* @param JWT token * @param secret * @return * @exception * */
public static Claims verify(String JWT, String secret){ Claims claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(JWT) .getBody(); return claims; } public static Object getValueFromToken(String jwt,String key, String secret){ return verify(jwt, secret).get(key); } }

ログインとログインの方法
/**
 * @author lirong
 * @ClassName: LoginController
 * @Description:   Controller
 * @date 2019-07-12 9:31
 */
@Slf4j
@RestController
@RequestMapping("/")
public class LoginController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtUtil jwtUtil;

    /**
     *   
     */
    @PostMapping("/login")
    public RestResult login(@RequestParam String username,
                            @RequestParam String password,
                            @RequestParam(required = false, defaultValue = "false") Boolean rememberMe,
                            HttpServletRequest request,
                            HttpServletResponse response) {
        Authentication authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));

        SecurityContextHolder.getContext()
                .setAuthentication(authentication);

        String jwt = jwtUtil.createJWT(authentication, rememberMe, false);
        String jwt_refresh = jwtUtil.createJWT(authentication, rememberMe, true);
        Map<String, String> map = new HashMap<>();
        map.put("token", jwt);
        map.put("refreshToken", jwt_refresh);

        CookieUtils.setCookie(response, "localhost", jwt);
        return ResultGenerator.genSuccessResult().setMessage("    ").setData(map);
    }

    /**
     *   
     * @param request
     * @return
     */
    @PostMapping("/logout")
    public RestResult logout(HttpServletRequest request) {
        try {
            //   JWT  
            jwtUtil.invalidateJWT(request);
        } catch (CustomException e) {
            throw new CustomException(ResultCode.UNAUTHORIZED);
        }
        return ResultGenerator.genSuccessResult().setMessage("    ");
    }

    /**
     *      token
     * @param refreshToken
     * @return
     */
    @PostMapping("/refresh/token")
    public RestResult refreshToken(String refreshToken) {
        Map<String, String> map;
        try {
            //   
            map = jwtUtil.refreshJWT(refreshToken);
        } catch (CustomException e) {
            throw new CustomException(ResultCode.TOKEN_EXPIRED);
        }
        return ResultGenerator.genSuccessResult().setMessage("token    ").setData(map);
    }
}

7.効果テスト
8.データベースとソース
上はプロジェクトの一部のコアコードだけで、完全なコードとデータベースはすでにGithubに管理されています.ソースリンクにアクセスして自分でダウンロードしてください.役に立つと思ったら、starを覚えてください.何か問題があれば、issuesやメールで交流してください.