前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >应用接jwt技术方案

应用接jwt技术方案

作者头像
叔牙
修改2021-12-24 10:37:40
5752
修改2021-12-24 10:37:40
举报

一、背景

新的业务应用,提供登录、权限、离线和在线业务的能力。内网环境。

对于以上使用场景,考虑了其他的一些登录方案,比如常见的自己手写登录、基于spring security、jwt以及spring security+jwt的解决方案,考虑到开发成本和技术成熟度,决定选择最后一种方案,并且在分析了jwt优缺点的基础上做了一些妥协和改造。

二、技术调研

1.jwt

jwt是一种无状态的前后端交互协议,概念和原理不做过多介绍,大致流程如下图

  • 登录

登录完成返回给client一个token(身份签名)

  • 验签

client访问服务端需带上token信息

2.spring security

三、技术方案

之所以选择jwt+security方案,是因为结合我们的具体业务场景,并考虑两种方案的优缺点以及应用场景,单独的一种都不太符合我们的诉求:

  • jwt:无状态,需要自己写认证和授权逻辑,并且对于用户修改密码后,服务端无法主动剔出颁发出去token(当然这个也不一定算得上缺点,仔细思考下stateless就知道了)
  • spring security:对于表单登录有比较完善解决方案,对于前后端分离场景,需要自定义实现

所以我们选择了jwt+spring security+version(服务端)这种轻状态的前后端交互协议。

实现过程中需要考虑的细节:

  1. token过期,自动过期与主动过期(密码修改)
  2. 在token失效前访问,自动续期,返回新token
  3. 登录完成需要返回用户菜单列表
  4. 认证和授权失败错误码

四、代码实现

1.引入依赖

代码语言:javascript
复制
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
  <version>2.0.5.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-jwt</artifactId>
  <version>1.0.10.RELEASE</version>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>${jjwt.version}</version>
</dependency>

2.jwttoken工具

代码语言:javascript
复制
public static String createToken(String username,String role) {

    Map<String,Object> map = new HashMap<>();
    map.put(ROLE_CLAIMS, role);

    String token = Jwts
            .builder()
            .setSubject(username)
            .setClaims(map)
            .claim("username",username)
            .setIssuedAt(new Date())
            .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
            .signWith(SignatureAlgorithm.HS256, APPSECRET_KEY).compact();
    return token;
}

public static Claims checkJWT(String token) {
    try {
        final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
        return claims;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

3.认证过滤器

代码语言:javascript
复制
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        super.setFilterProcessesUrl("/auth/login");
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request,
                                                HttpServletResponse response) throws AuthenticationException {

        // 从输入流中获取到登录的信息
        try {
            User loginUser = new ObjectMapper().readValue(request.getInputStream(), User.class);
            return authenticationManager.authenticate(
                    new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword())
            );
        } catch (IOException e) {
            e.printStackTrace();
            return null;
        }
    }

    // 成功验证后调用的方法
    // 如果验证成功,就生成token并返回
    @Override
    protected void successfulAuthentication(HttpServletRequest request,
                                            HttpServletResponse response,
                                            FilterChain chain,
                                            Authentication authResult) throws IOException, ServletException {

        JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
        System.out.println("jwtUser:" + jwtUser.toString());

        String role = "";
        Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
        for (GrantedAuthority authority : authorities){
            role = authority.getAuthority();
        }

        String token = JwtUtil.createToken(jwtUser.getUsername(), role);
        //String token = JwtTokenUtils.createToken(jwtUser.getUsername(), false);
        // 返回创建成功的token
        // 但是这里创建的token只是单纯的token
        // 按照jwt的规定,最后请求的时候应该是 `Bearer token`
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        String tokenStr = JwtUtil.TOKEN_PREFIX + token;
        response.setHeader("token",tokenStr);
    }

    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        response.getWriter().write("authentication failed, reason: " + failed.getMessage());
    }

}

4.授权过滤器

代码语言:javascript
复制
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {

    public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

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

        String tokenHeader = request.getHeader(JwtUtil.TOKEN_HEADER);
        // 如果请求头中没有Authorization信息则直接放行了,交给后边的认证拦截
        if (tokenHeader == null || !tokenHeader.startsWith(JwtUtil.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果请求头中有token,则进行解析,并且设置认证信息
        SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
        super.doFilterInternal(request, response, chain);
    }

    // 这里从token中获取用户信息并新建一个token
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
        String token = tokenHeader.replace(JwtUtil.TOKEN_PREFIX, "");
        //todo 如果有篡改token,捕获SignatureException并返回
        String username = JwtUtil.getUsername(token);
        String role = JwtUtil.getUserRole(token);
        if (username != null){
            return new UsernamePasswordAuthenticationToken(username, null,
                    Collections.singleton(new SimpleGrantedAuthority(role))
            );
        }
        return null;
    }
}

5.权限拦截配置

代码语言:javascript
复制
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    @Qualifier("jwtUserDetailService")
    private UserDetailsService userDetailsService;

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable()
                .authorizeRequests()
                // 测试用资源,需要验证了的用户才能访问
                .antMatchers("/tasks/**")
                .authenticated()
                .antMatchers(HttpMethod.DELETE, "/tasks/**")
                .hasRole("ADMIN")
                // 其他都放行了
                .anyRequest().permitAll()
                .and()
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .addFilter(new JWTAuthorizationFilter(authenticationManager()))
                // 不需要session
                .sessionManagement()
                .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling()
                .authenticationEntryPoint(new JwtAuthenticationEntryPoint());
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        final UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", new CorsConfiguration().applyPermitDefaultValues());
        return source;
    }
}

总结

本篇实现基于spring-security能力,很好的复用了其权限模块,当然我们也可以完全自己实现,把权限认证和授权逻辑通过拦截器的方式嵌入到请求流中,并且在一些内部系统或者登录态要求不是很强的场景都可以使用jwt方案来实现简单的认证和授权,当然jwt相比于oauth2.0更像是其其中一个小而美的模块能力,一些复杂的场景或者平台级别的应用,建议使用CAS来做统一门面,或者oauth2.0来做对外权限认证。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2021-12-22,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 PersistentCoder 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、背景
  • 二、技术调研
    • 1.jwt
      • 2.spring security
      • 三、技术方案
      • 四、代码实现
        • 1.引入依赖
          • 2.jwttoken工具
            • 3.认证过滤器
              • 4.授权过滤器
                • 5.权限拦截配置
                • 总结
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档