SpringSecurity前面讲解的一些配置都是基于前后端都是一起的,那么当分开了的时候就会出现一系列问题,跨域之类的问题随之而出 所以出现了jwt帮助我们完成SpringSecurity前后端分离权限控制
jwt (json web Token)它是基于RFC 7519开放标准用于双方安全展示信息的一种方式。通俗说就是是用于服务端和客户端相互交换信息的一种凭证。如果用过aouth2的小伙伴本对这个应该不陌生Token。
在传统模式当中我们的认证流程是
用户登录->服务端生成session->cookie ->服务端写代session->查找用户->findOK

1.服务端需一定资源保存session信息,用户多时资源消耗较大
扩展性不好,当我们的服务端需要集群时,
2.因session保存在服务端,此时无法定位session,造成登录失效
3.跨域问题,当我们访问A网站时,此时不想再登录就能够访问关联网站B。(传统解决办法:写入持久层,A,B同时访问)
所以现在可以采用的解决办法,不需要服务端去保存session,
用户登录--->服务端生成凭证->携带凭证带客户端——>客户端保存凭证->每次客户端请求都携代凭证,客户端通过则验证成功,调用API获取信息。所以就有了jwt的诞生

jwt的组成部分

  1. header(头),保存算法,类型
  2. payload(负载),用户的信息,如id,用户名等等
  3. signature(签名),将生成的token编码(加密)
    他们之间用 “.”号隔开。

新建springboot项目
引入pom

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-data-jpa</artifactId>
  4. </dependency>
  5. <dependency>
  6. <groupId>org.springframework.boot</groupId>
  7. <artifactId>spring-boot-starter-security</artifactId>
  8. </dependency>
  9. <dependency>
  10. <groupId>org.springframework.boot</groupId>
  11. <artifactId>spring-boot-starter-web</artifactId>
  12. </dependency>
  13. <dependency>
  14. <groupId>mysql</groupId>
  15. <artifactId>mysql-connector-java</artifactId>
  16. <scope>runtime</scope>
  17. </dependency>
  18. <dependency>
  19. <groupId>org.projectlombok</groupId>
  20. <artifactId>lombok</artifactId>
  21. <optional>true</optional>
  22. </dependency>
  23. <dependency>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-test</artifactId>
  26. <scope>test</scope>
  27. </dependency>
  28. <dependency>
  29. <groupId>org.springframework.security</groupId>
  30. <artifactId>spring-security-test</artifactId>
  31. <scope>test</scope>
  32. </dependency>
  33. <dependency>
  34. <groupId>io.jsonwebtoken</groupId>
  35. <artifactId>jjwt</artifactId>
  36. <version>0.9.1</version>
  37. </dependency>

编写yml文件

  1. server:
  2. port: 8080
  3. spring:
  4. datasource:
  5. driver-class-name: com.mysql.cj.jdbc.Driver
  6. username: root
  7. password: root
  8. url: jdbc:mysql://localhost:3306/security_db?useSSL=false&serverTimezone=UTC
  9. jpa:
  10. show-sql: true
  11. hibernate:
  12. ddl-auto: update
  13. properties:
  14. hibernate:
  15. format_sql: true
  16. logging:
  17. level:
  18. org.springframework.*: debug

参考之前文章,编写实体和repository这里就不放代码了 直接放截图
SpringSecurity整合JWT - 图1
因为是前后端分离的所有封装了一个专们响应前台数据的实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResponseDto<V> {


    private int code;

    private String msg;

    private V data;
}
//编写securityConfig


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true,securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //添加异常
    @Resource
    private TokenExceptionHandler tokenExceptionHandler;

    @Resource
    private AccessDeniedHandler accessDeniedHandler;

    @Resource
    private JwtTokenFilter jwtTokenFilter;
//获取Token的接口不必拦截
    @Override
    public void configure(WebSecurity web) throws Exception {

        web.ignoring().antMatchers( HttpMethod.GET,"/token" );
         }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //前后端分离所以不要考虑csrf可以禁用掉
        http.csrf().disable()
                //添加异常处理
                .exceptionHandling().authenticationEntryPoint( tokenExceptionHandler )

                .accessDeniedHandler( accessDeniedHandler )
                //
                .and().sessionManagement().sessionCreationPolicy( SessionCreationPolicy.STATELESS )
                //拦截所有请求
                .and().authorizeRequests().anyRequest().authenticated();
        //定义filter addFilterAt  用于filter替换
        http.addFilterAt(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
        //super.configure( http );
    }
}

//对没有Token 的请求进行拦截  编写handler处理

//验证没有token异常
@Component
public class TokenExceptionHandler implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {

        // 直接返回 json错误
        ResponseDto <Object> result = new ResponseDto<>();
        //20,标识没有token
        result.setCode(20);
        result.setMsg("请求无效,没有有效token");

        ObjectMapper objectMapper = new ObjectMapper();

        response.getWriter().write(objectMapper.writeValueAsString(result));
    }
}

//访问被拒绝处理
@Component
public class AccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler {


    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        // 返回我们的自定义json
        ObjectMapper objectMapper = new ObjectMapper();
        ResponseDto <Object> result = new ResponseDto<>();
        //50,标识有token,但是该用户没有权限
        result.setCode(50);
        result.setMsg("请求无效,没有有效token");
        response.getWriter().write(objectMapper.writeValueAsString(result)); // 返回我们的自定义json
    }
}

//自定义Filter
@Component
public class JwtTokenFilter extends OncePerRequestFilter {
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
         request.setCharacterEncoding("UTF-8");
        response.setCharacterEncoding("UTF-8");
        String token = request.getHeader("token");

        //获取token,并且解析token,如果解析成功,则放入 SecurityContext
        if (token != null) {
            try {
                AuthUser authUser = JwtUtil.parseToken(token);
                //todo: 如果此处不放心解析出来的 authuser,可以再从数据库查一次,验证用户身份:

                //解析成功
                if (SecurityContextHolder.getContext().getAuthentication() == null) {
                    //我们依然使用原来filter中的token对象
                    UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(authUser, null, authUser.getAuthorities());

                    SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                }
            } catch (Exception e) {
                logger.info("解析失败,可能是伪造的或者该token已经失效了(我们设置失效5分钟)。");
            }
        }

        filterChain.doFilter(request, response);
    }
}

controller代码

@RestController
public class UserController {


    @Resource
    private UserRepository userRepository;
    @Resource
    private RoleRepository roleRepository;

    @GetMapping("/token")
    public ResponseDto login(String username, String password) {
        User user = userRepository.findByUsername(username);

        if (user == null || !user.getPassword().equals(password)) {
            ResponseDto <Object> result = new ResponseDto <>();
            result.setCode(10);
            result.setMsg("用户名或密码错误");
            return result;
        }

        ResponseDto <Object> success = new ResponseDto <>();
        //用户名密码正确,生成token给客户端
        success.setCode(0);
        List <Role> roles = Collections.singletonList(roleRepository.findById(user.getId()).get());
        success.setData( JwtUtil.generateToken(username, roles));

        return success;
    }
}

@RestController
@RequestMapping
public class PermissionController {
    @GetMapping("/permission")
    public ResponseDto loginTest(@AuthenticationPrincipal AuthUser authUser) {
        ResponseDto<String> resultVO = new ResponseDto<>();
        resultVO.setCode(0);

        resultVO.setData("你成功访问了该api,这代表你已经登录,你是: " + authUser);
        return resultVO;
    }

    @GetMapping("/loginUser")
    @PreAuthorize("hasRole('user')")
    public ResponseDto loginTest() {
        ResponseDto<String> resultVO = new ResponseDto<>();
        resultVO.setCode(0);

        resultVO.setData("你成功访问了需要有 user 角色的api。");
        return resultVO;
    }
}

采用postman进行测试 直接放图
SpringSecurity整合JWT - 图2

SpringSecurity整合JWT - 图3

带入token在header里面去访问 不需要角色的接口
SpringSecurity整合JWT - 图4
SpringSecurity整合JWT - 图5

新建一个没有该角色的用户去访问
SpringSecurity整合JWT - 图6

总结:jwt+security整合流程

  1. 引入jwt security的pom文件
  2. 创建实体 实现userDetail 和security交互的实体类
  3. 自定义需要处理的异常 访问被拒绝的异常等、看个人所需
    4.编写filter 自定义filter 去验证token是否通过 ,以及保证失效token不会进入接口
  4. 编写securityconfig 配置websecurity 让获取token接口绕过security 编写httpsecurity设置设置对web端所有的都进行拦截 都需要经过验证才行,添加异常处理
    添加对filter替换。替换掉UsernamePasswordAuthenticationFilter 该filter默认情况响应的是/login