Spring Security + JWT 完成RBAC动态授权

此篇文章为spring security系列的第一篇,着重讲解如何通过spring security完成企业级项目的权限控制,以及采用Redis的方式控制JWT的失效。

1. 什么是RBAC

RBAC(Role-Based Access Control )基于角色的权限控制,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。

2. JWT 和 Spring Security

spring security 授权主要分为两种,一种是security内部负责维护登录用户的session,一种则是采用JWT的方式,不管理session。关于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也是目前比较流行安全的一种加密方式,它比MD5效率更高),userDetailsService则负责对用户名、密码的校验和授权。

2. HttpSecurity http 主要是对security核心过滤器链的配置,可配置登录、登出及异常等处理器,因为我们采用的是JWT的方式,因此禁用了security提供的登录和登出,配置了JWT的过滤器,以及RBAC校验的方式。

3. WebSecurity web主要负责配置一些security放行的路径,文中通过customConfig读取在配置文件中设置的放行的URL。

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的形式,及登录的时候创建两个JWT,一个token,一个refresh_token,refresh_token的过期时间设置比较长,token失效后,前端可调用refresh_token的接口来刷新来达到续签的目的。

/**
 * jwt工具类
 * @author  lirong
 * @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<br/>
     * @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<br/>
     * @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上,请访问:https://github.com/Janche/spring-security-rbac-jwt.git自行下载,觉得有用的话,记得star哦,有什么问题欢迎大家通过issues或者邮件进行交流。

<think>好的,我需要处理用户关于Spring SecurityJWT实现权限认证的面试题请求。首先,用户提到了三个引用内容,我需要仔细阅读这些引用,确保答案中的信息能够正确引用这些来源。 根据引用[1],里面提到了RBAC的权限配置、Spring Security的基本使用,以及结合JWT实现前后端分离,包括JSON登录和密码加密。引用[2]和[3]则涉及具体的配置类,如继承WebSecurityConfigurerAdapter,以及JWT的加密方式和刷新令牌机制。特别是引用[3]提到了refreshToken解决令牌续期问题,还有加密方式的问题。 接下来,我需要生成相关的面试题。用户可能希望涵盖基础概念、配置步骤、核心组件、常见问题及解决方案等方面。需要确保问题覆盖Spring SecurityJWT的整合流程、安全配置、权限管理、令牌刷新机制、加密方法等关键点。 然后,考虑问题的结构。通常面试题会分为基础概念、配置与整合、权限控制、安全机制、优化与扩展等部分。每个部分下需要设计具体的问题,确保问题有层次,从浅入深。 例如,基础概念部分可以问JWT的结构和优势,Spring Security的作用。配置与整合部分涉及如何配置Spring SecurityJWT,核心配置类是什么。权限控制会涉及RBAC模型和注解的使用。安全机制可能包括密码加密、CSRF保护、令牌刷新。优化部分可能涉及性能优化和分布式系统的处理。 同时,要确保答案中的引用正确,比如提到令牌刷新机制时要引用[3],密码加密引用[1],配置类引用[2]。还要注意使用中文回答,行内公式用$...$,独立公式用$$...$$,但在这个问题中可能公式较少,但需要检查是否有数学表达式的情况。 最后,生成相关问题时要覆盖用户可能关心的各个方面,帮助用户全面准备面试。需要确保问题数量适中,涵盖主要知识点,并且问题之间有逻辑连贯性。</think>### Spring SecurityJWT实现权限认证面试题解析 #### 1. **基础概念** **Q1: JWT的结构是什么?它在认证中有什么优势?** A1: JWT由三部分组成:头部(Header)、载荷(Payload)、签名(Signature),格式为`Header.Payload.Signature`。优势包括: - **无状态**:服务端无需存储会话信息[^3]; - **跨域支持**:适合前后端分离架构[^1]; - **自包含性**:载荷可存储用户基本信息(如角色、权限)。 **Q2: Spring Security的核心功能是什么?** A2: Spring Security提供身份验证(Authentication)和授权(Authorization)功能,支持RBAC(基于角色的访问控制)模型[^1],可拦截请求并验证用户权限。 --- #### 2. **配置与整合** **Q3: 如何配置Spring SecurityJWT整合?** A3: 关键步骤包括: 1. **继承`WebSecurityConfigurerAdapter`**:覆盖`configure`方法,定义安全规则和过滤器链[^2]。 2. **自定义`JwtAuthenticationFilter`**:拦截请求并解析JWT令牌。 3. **配置密码加密**:使用`BCryptPasswordEncoder`[^1]。 4. **禁用Session**:通过`sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)`实现无状态。 **Q4: Spring Security中`UserDetailsService`的作用是什么?** A4: 该接口用于加载用户信息(如用户名、密码、权限),需实现`loadUserByUsername`方法,从数据库或其他存储中查询用户数据[^1]。 --- #### 3. **权限控制** **Q5: 如何实现基于RBAC的权限控制?** A5: - **角色表设计**:用户表、角色表、权限表通过中间表关联[^1]。 - **注解控制**:使用`@PreAuthorize("hasRole('ADMIN')")`在方法或接口上声明权限。 - **动态配置**:通过`HttpSecurity`的`antMatchers()`配置URL路径权限[^2]。 **Q6: 如何实现接口级别的权限校验?** A6: 结合`@PreAuthorize`注解和SpEL表达式,例如: ```java @PostMapping("/admin") @PreAuthorize("hasAuthority('ADMIN_WRITE')") public void adminOperation() { ... } ``` --- #### 4. **安全机制与优化** **Q7: JWT如何解决令牌过期问题?** A7: 使用**刷新令牌(Refresh Token)**机制: -JWT即将过期时,客户端用Refresh Token请求新JWT- Refresh Token有效期较长,但需存储于服务端或加密签名。 **Q8: JWT的签名算法有哪些?如何防止令牌篡改?** A8: 常用算法包括HMAC(如HS256)和RSA(如RS256)。签名通过密钥生成,验证时重新计算签名以检测篡改。 **Q9: 如何防御CSRF攻击?** A9: Spring Security默认启用CSRF保护,但若使用JWT的无状态架构,需显式禁用: ```java @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); } ``` --- #### 5. **常见问题与扩展** **Q10: 分布式系统中JWT如何实现注销?** A10: 传统JWT无法直接注销,可通过以下方案: - **短期令牌**:缩短JWT有效期,依赖Refresh Token续期。 - **黑名单**:服务端维护失效令牌列表,但需额外存储。 **Q11: Spring Security如何整合OAuth2?** A11: 使用`spring-security-oauth2`库,配置授权服务器(`AuthorizationServerConfigurer`)和资源服务器(`ResourceServerConfigurer`),支持OAuth2协议与JWT结合。 --- ###
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值