SpringBoot整合后端项目-3.集成鉴权(JWT+Security) - Go语言中文社区

SpringBoot整合后端项目-3.集成鉴权(JWT+Security)


前言

该系列文章,是我从零搭建一套后端项目的经验总结。
没有过多的流程梳理和细节描写,目的也是对所学技术的一个总结和巩固。
内容大多包括实现思路,实现要点、注意事项和一些个人心得,希望对各位看官有所帮助。

转载请注明来源

1.实现思路

  • 注入相关依赖
  • 编写通过JWT 生成Token和解析Token的两个方法
  • 路由配置:继承 WebSecurityConfigurerAdapter 类重写
    configure(HttpSecurity http) 方法,自定义路由配置(定义哪些路由需要校验,登陆接口不校验),并使用自定义过滤器进行权限校验
  • 登录接口,获取参数进行用户名密码校验,校验通过调用生成Token方法,可以将部分用户信息放在 SecurityContextsetAuthentication 中。
  • 编写自定义过滤器类,类内调用 解析Token的方法,并加一些自己的业务逻辑判断。Token解析成功,业务逻辑判断通过,且security的默认过滤器链都校验通过,则鉴权通过,重定向到具体的路由接口处理前台请求。
    注意:
    若没有走security的UsernamePasswordAuthenticationToken、UserDetailsService的那一套验证。
    要重写 AuthenticationProvider 的 authenticate(验证)方法,自定义security的权限验证。 
    否则会报错 Bad credentials

2.实现要点

1.依赖注入:

<!--security-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--JWT-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>${jjwt.version}</version>
</dependency>

2.TokenAuthentication类:有创建Token和解析Token的两个方法以及相关常量的定义

public class TokenAuthentication {
    private static final long EXPIRATION_TIME = 86_400_000; // 1天
    private static final String SECRET = "P@Xue"; // JWT密码
    private static final String TOKEN_PREFIX = "Bearer"; // Token前缀
    private static final String HEADER_STRING = "Authorization";// 存放Token的Header Key

    /*根据jwt创建token*/
    public static String creatTokenByJwt(String userInfo) {
        String[] userInfoA = userInfo.split(",");
        return Jwts.builder()
                .claim("userId", userInfoA[0])
                .claim("userName", userInfoA[1])
                .claim("address", userInfoA[2])
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS512, SECRET)
                .compact();
    }

    /*解析token*/
    public Authentication parseToken(HttpServletRequest request, HttpServletResponse response) {
        String token = request.getHeader(HEADER_STRING);
        if (Tools.notEmpty(token)) {
            // 解析 Token
            Claims claims = Jwts.parser()
                    // 验签
                    .setSigningKey(SECRET)
                    // 去掉 Bearer
                    .parseClaimsJws(token.replace(TOKEN_PREFIX, "")).getBody();
            String userId = claims.get("userId").toString();
            //具备访问权限 重新生成jwt
            if (checkUrl(userId, request)) {
                String userInfo = claims.get("userId").toString() + "," + claims.get("userName").toString() + "," + claims.get("address").toString();
                token = creatTokenByJwt(userInfo);
                response.setHeader(HEADER_STRING, token);
                return new UsernamePasswordAuthenticationToken(userInfo,null);
            }
            return null;
        }
        return null;
    }

    /**
     * 根据userId从数据库中取出该用户对应的API权限,然后与request中的请求API做比较,检验是否具备访问权限
     * @param userId
     * @param request
     * @return
     */
    private boolean checkUrl(String userId, HttpServletRequest request) {
        return "1".equals(userId);
    }
}

3.登录接口类,验证用户名和密码。通过后调用自定义的authenticate()方法。然后userInfo信息生成token

@Override
public Result login(UserPO userPO, HttpServletRequest request) {
    //根据用户名查询出来用户信息
    UserPO msg =  userMP.login(userPO);
    if (msg == null) {
        return Result.error("用户不存在");
    }
    if (!Md5Utils.hash(userPO.getPassword()).equals(msg.getPassword())) {
        return Result.error("密码错误");
    }
    String userInfo = msg.getUserId() + "," + msg.getUserName() + "," + msg.getAddress();
    //使用自定义的authenticate验证
    Authentication authentication = this.authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(userInfo,null));
    //根据jwt和用户信息生成token
    String token = TokenAuthentication.creatTokenByJwt(authentication.getName());
    return Result.ok("登陆成功").put("token",token);
}

4.自定义的AuthenticationProvider实现类authenticate。

这里开始对security的理解不够透彻,一直报错 Bad credentials,找不到原因。 网上的解决方案,大多是用户名、密码错误之类的。 以这个错误为出发点,查阅了相关资料,了解了其原理和源码,重写authenticate方法解决问题。

@Component
public class MyAuthenticationProvider implements AuthenticationProvider {

   @Override
   public Authentication authenticate(Authentication authentication) throws AuthenticationException {
      // 获取认证的用户名 & 密码
      Authentication auth = new UsernamePasswordAuthenticationToken(authentication.getPrincipal(),
            authentication.getCredentials());
      SecurityContextHolder.getContext().setAuthentication(authentication);
      return auth;
   }
   // 是否可以提供输入类型的认证服务
   @Override
   public boolean supports(Class<?> authentication) {
      return authentication.equals(UsernamePasswordAuthenticationToken.class);
   }
}

5.重写WebSecurityConfigurerAdapter类的configure方法

package com.example.frame.config;

import com.example.frame.common.TokenAuthentication;
import com.example.frame.common.filter.JWTAuthenticationFilter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * security的路由配置
 *
 * @Author: Jie Li
 * @CreateTime: 2019/9/10.
 */
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    JWTAuthenticationFilter jWTAuthenticationFilter() {
        return new JWTAuthenticationFilter();
    }
    @Bean
    TokenAuthentication tokenAuthenticationSV() {
        return new TokenAuthentication();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 关闭csrf验证
        http.csrf().disable()
                // 对请求进行认证
                .authorizeRequests()
                //所有的跨域请求全部打开
                .antMatchers(HttpMethod.OPTIONS, "/**").permitAll()
                .antMatchers("/inner/**").permitAll()
                .antMatchers("/swagger-ui.html").permitAll()
                .antMatchers("/swagger-resources").permitAll()
                .antMatchers("/beans").permitAll()//显示所有的spring bean
                .antMatchers("/autoconfig").permitAll()//显示自动配置信息
                .antMatchers("/configprops").permitAll()//显示配置属性列表
                .antMatchers("/dump").permitAll()//显示线程活动的快照
                .antMatchers("/env").permitAll()//显示应用程序的环境变量
                .antMatchers("/health").permitAll()//显示应用程序的健康指标
                .antMatchers("/info").permitAll()//显示应用的信息
                .antMatchers("/mappings").permitAll()//显示所有的URL路径
                .antMatchers("/metrics").permitAll()//显示应用的度量标准信息
                .antMatchers("/trace").permitAll()//显示跟踪信息
                .antMatchers("/druid/**").permitAll()//显示连接池
                // 所有 /login 的POST请求 都放行
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                .antMatchers(HttpMethod.GET, "/v2/api-docs").permitAll()
                // 所有请求需要身份认证
                .anyRequest().authenticated().and()
                // 添加一个过滤器验证其他请求的Token是否合法
                .addFilterBefore(jWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 忽略静态资源
     *
     * @param web
     * @throws Exception
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/webjars/**")
                .antMatchers("/swagger-resources/**")
                .antMatchers("/file/**");
        // 可以仿照上面一句忽略静态资源
    }
}

注意:要将自己定义的过滤器和Token管理类注入到bean容器中。

// 添加一个过滤器验证其他请求的Token是否合法
.addFilterBefore(jWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);

添加自定义的过滤器进行校验。

6.自定义过滤器类:JWTAuthenticationFilter,重写doFilter方法:

public class JWTAuthenticationFilter extends GenericFilterBean {
    @Autowired
    TokenAuthentication tokenAuthentication;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
            throws IOException, ServletException {
        Authentication authentication = tokenAuthentication.parseToken((HttpServletRequest) request, (HttpServletResponse) response);
        SecurityContextHolder.getContext().setAuthentication(authentication);
        filterChain.doFilter(request, response);
    }
}

注意
1.先调用解析Token方法parseToken(),鉴权通过后。调用SecurityContextHolder.getContext().setAuthentication(authentication);
将权限信息(会包括一些基本的用户信息)放入SecurityContext管理的authentication中。
可以通过 **SecurityContextHolder.getContext().getAuthentication();**获取到权限信息。
方法有以下几种:
在这里插入图片描述
2.Token解析,自己的逻辑校验完成后,会执行

 filterChain.doFilter(request, response);

也就是执行下一个过滤器,按照security默认的过滤器链执行。
鉴权成功,执行完过滤器之后,就会重定向到我们的请求接口上,执行我们具体的业务请求:

@RestController
@RequestMapping("/api/user")
public class UserCL {

    private final UserSV userSV;

    @Autowired
    public UserCL(UserSV userSV) {
        this.userSV = userSV;
    }

    @GetMapping("/getName")
    public UserPO getName(String id) {
        return userSV.getName(id);
    }
}

异常处理:Bad credentials

在这里插入图片描述
解决方案:
1.若用了 UsernamePasswordAuthenticationToken、UserDetailsService 去进行校验,则去检查 用户名不存在,密码错误等情况。

2.若没有使用 UsernamePasswordAuthenticationToken 去校验,自定义了过滤器去校验,则必须要重写 AuthenticationProvider 的 authenticate(验证)方法。上文中有提及。

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/rape_flower/article/details/100774554
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢