SpringSecurity整合JWT实现访问控制

21 篇文章 0 订阅
15 篇文章 0 订阅

前置知识点

整体流程

  1. 用户向服务器发送用户名、密码以及验证码用于登陆系统。
  2. 如果用户用户名、密码以及验证码校验正确的话,服务端会返回已经签名的 Token,也就是 JWT。
  3. 用户以后每次向后端发请求都在 Header 中带上这个 JWT 。
  4. 服务端检查 JWT 并从中获取用户相关信息。

如下图所示:
在这里插入图片描述

JWT (JSON Web Token)简单介绍

本文主要利用JWT实现登录授权,一旦登录之后,就返回给前端一个Token,利用Token进行信息交互。

Token由三部分组成:Header、Payload、Signature

  • Header:通常由两部分组成:
    • typ(Type):令牌类型,也就是 JWT。
    • alg(Algorithm) :签名算法,比如 HS256。
  • Payload:Payload 也是 JSON 格式数据,其中包含了 Claims(声明,包含 JWT 的相关信息)。一般不存放隐私信息
  • Signature:Signature 部分是对前两部分的签名,作用是防止 JWT(主要是 payload) 被篡改。

Spring Security 简单流程介绍

翻看源码可知,Spring Security通过职责链模式,通过各类XXXFilter对请求进行了拦截。实际操作时经常需要实现XXXFilter来自定义的登录以及访问控制。在这里插入图片描述

具体实现(仅展示关键实现类)

测试接口

/**
 * @author qyl
 * @program HelloController.java
 * @Description Test for diff auths
 * @createTime 2022-07-12 10:24
 */
@RestController
@RequestMapping("/test")
public class TestController {
    @ApiOperation(value = "hello")
    @PostMapping("hello")
    @PreAuthorize("hasAuthority('hello')")
    public R hello() {
        return R.ok().data("hello", "hello");
    }

    @ApiOperation(value = "manage")
    @PostMapping("manage")
    @PreAuthorize("hasAuthority('manage')")
    public R manage() {
        return R.ok().data("manage", "manage");
    }
}

自定义访问过滤器:

/**
 * <p>
 * 访问过滤器
 * 首先获取header中的token 判断是否已经登录;
 * 1.没登陆 抛异常
 * 2.登录过了 就将授权信息一起放入安全上下文中
 * </p>
 *
 * @author qyl
 * @since 2022-7-09
 */
public class TokenAuthenticationFilter extends BasicAuthenticationFilter {
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenAuthenticationFilter(AuthenticationManager authManager, TokenManager tokenManager,RedisTemplate redisTemplate) {
        super(authManager);
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }
	
    @Override
    protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        UsernamePasswordAuthenticationToken authentication = null;
        authentication = getAuthentication(req);

        if (authentication != null) {
            SecurityContextHolder.getContext().setAuthentication(authentication);
        } else {
            ResponseUtil.out(res, R.error());
        }
        chain.doFilter(req, res);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
        // token置于header里
        String token = request.getHeader("token");
        if (token != null && !"".equals(token.trim())) {
            String userName = tokenManager.getUserFromToken(token);

            List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
            Collection<GrantedAuthority> authorities = new ArrayList<>();
            for(String permissionValue : permissionValueList) {
                if(StringUtils.isEmpty(permissionValue)) continue;
                SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
                authorities.add(authority);
            }

            if (!StringUtils.isEmpty(userName)) {
                return new UsernamePasswordAuthenticationToken(userName, token, authorities);
            }
            return null;
        }
        return null;
    }
}

自定义登录过滤器:

/**
 * <p>
 * 登录过滤器,继承UsernamePasswordAuthenticationFilter,
 * 通过对Post发送的/login请求进行拦截,对用户名密码进行登录校验
 * </p>
 *
 * @author qyl
 * @since 2022-7-09
 */
public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;
    private TokenManager tokenManager;
    private RedisTemplate redisTemplate;

    public TokenLoginFilter(AuthenticationManager authenticationManager, TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.authenticationManager = authenticationManager;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
        this.setPostOnly(false);
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/login","POST"));
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse res)
            throws AuthenticationException {
        String username = req.getParameter("username");
        String password = req.getParameter("password");
        User user = new User();
        user.setUsername(username);
        user.setPassword(password);
        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
    }

    /**
     * 登录成功将用户信息放入redis中: 
     *    username 作为 key
     *    permissionValueList 作为 value
     * 之后将token返回以便在访问控制时从redis中取出权限,实现访问控制
     * 
     * @param req
     * @param res
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse res, FilterChain chain,
                                            Authentication auth) throws IOException, ServletException {
        SecurityUser user = (SecurityUser) auth.getPrincipal();
        String token = tokenManager.createToken(user.getCurrentUserInfo().getUsername());
        redisTemplate.opsForValue().set(user.getCurrentUserInfo().getUsername(), user.getPermissionValueList());
        ResponseUtil.out(res, R.ok().data("token", token));
    }

    /**
     * 登录失败
     * @param request
     * @param response
     * @param e
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response,
                                              AuthenticationException e) throws IOException, ServletException {
        ResponseUtil.out(response, R.error());
    }
}

自定义登录过程中账号密码查询方式(不写就不好从数据库获取账号密码)

/**
 * <p>
 * 自定义userDetailsService - 认证用户详情
 * </p>
 */

@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserService userService;

    @Autowired
    private PermissionService permissionService;

    /***
     * 根据账号获取用户信息
     * @param username:
     * @return: org.springframework.security.core.userdetails.UserDetails
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库中取出用户信息
        User user = userService.selectByUsername(username);

        // 判断用户是否存在
        if (null == user){
            throw new UsernameNotFoundException("用户名不存在!");
        }
        // 返回UserDetails实现类
        User curUser = new User();
        BeanUtils.copyProperties(user,curUser);

        List<String> authorities = permissionService.selectPermissionValueByUserId(user.getId());
        SecurityUser securityUser = new SecurityUser(curUser);
        securityUser.setPermissionValueList(authorities);
        return securityUser;
    }

Security 配置类

/**
 * <p>
 * Security配置类
 * </p>
 *
 * @author qyl
 * @since 2022-7-09
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private UserDetailsService userDetailsService;
    private TokenManager tokenManager;
    private DefaultPasswordEncoder defaultPasswordEncoder;
    private RedisTemplate redisTemplate;

    @Autowired
    public SecurityConfig(UserDetailsService userDetailsService, DefaultPasswordEncoder defaultPasswordEncoder,
                          TokenManager tokenManager, RedisTemplate redisTemplate) {
        this.userDetailsService = userDetailsService;
        this.defaultPasswordEncoder = defaultPasswordEncoder;
        this.tokenManager = tokenManager;
        this.redisTemplate = redisTemplate;
    }

    /**
     * 配置设置
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.exceptionHandling()			// 如果有异常 直接就抛出 不再跳转到登录页面
                .authenticationEntryPoint(new UnauthorizedEntryPoint())
                .and().csrf().disable()		//关闭csrf
                .authorizeRequests()		
                .anyRequest().authenticated()	// 对所有的请求都进行验证
                .and().addFilter(new TokenLoginFilter(authenticationManager(), tokenManager, redisTemplate))					// 增加两个过滤器 职责链模式
                .addFilter(new TokenAuthenticationFilter(authenticationManager(), tokenManager, redisTemplate)).httpBasic();
    }

    /**
     * 密码处理 这里通过注入的userDetailsService来实现Form传来的账号密码与数据库账号密码进行比对
     *
     * @param auth
     * @throws Exception
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(defaultPasswordEncoder);
    }

    /**
     * 配置哪些请求不拦截
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/v2/**", "/api/**");
    }
}

实际运行流程解析

访问控制流程解析

  1. 发送一个hello请求在这里插入图片描述
  2. 进入TokenAuthenticationFilter判断用户是否登录 如果存在token了(已经登录了),就把用户权限封装到UsernamePasswordAuthenticationToken中,把该UsernamePasswordAuthenticationToken放入安全信息上下文,以便后面存取。在这里插入图片描述
  3. 之后执行下一个Filter在这里插入图片描述
  4. 最后进入SecurityContextPersistenceFilter,即在所有的Filter执行完毕后,把安全信息保存起来,做个持久化在这里插入图片描述
  5. 当保存完安全信息之后就放行,开始请求TestController接口中的hello方法,并开始权限验证。在这里插入图片描述
    在这里插入图片描述
    整个过程就是这样,这里test用户没有manage权限,请求自然失败了。在这里插入图片描述在这里插入图片描述

登录流程解析

  1. 发送post请求
    在这里插入图片描述
  2. 进入TokenLoginFilter把信息传入ProviderManager中进行数据校验。在这里插入图片描述
    这里用到了上述我们自定义的UserDetailsService来查询信息。
    在这里插入图片描述
  3. 在比对成功之后就进入successfulAuthentication方法,实现自己的一些业务逻辑。
    在这里插入图片描述
  4. 再看redis已经将username和permissionList存进去了在这里插入图片描述

总结

JWT需要注意细节

  1. 使用安全系数高的加密算法。
  2. 使用成熟的开源库,没必要造轮子。
  3. JWT 存放在 localStorage 中而不是 Cookie 中,避免 CSRF 风险。
  4. 一定不要将隐私信息存放在 Payload 当中。密钥一定保管好,一定不要泄露出去。
  5. JWT 安全的核心在于签名,签名安全的核心在密钥。
  6. Payload 要加入 exp (JWT 的过期时间),永久有效的 JWT 不合理。并且,JWT 的过期时间不易过长。

完整代码链接

github代码链接

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值