SpringSecurity简介

SpringSecurity是Spring安全框架中的一员,在SpringBoot出现之前,SpringSecurity已经发展了许久了,但使用并不多,这个领域一直都是Shiro的天下。

相对于Shiro,在SSM/SSH中整合SpringSecurity都是比较麻烦的,所以即使SpringSecurity功能比Shiro强大,但使用反而没Shiro多(虽然Shiro功能没用SpringSecurity多,但绝大部分项目而言,已经够用了)。

SpringBoot出来以后,对SpringSecurity提供了自动化配置方案,可以零配置使用SpringSecurity。

所以常见的技术栈组合是如下:

  • SSM + Shiro
  • SpringBoot + SpringSecurity

回到SpringSecurity,它的核心实现就是维护了一组过滤器链。

基本使用

创建项目,引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!--可选-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-test</artifactId>
    <scope>test</scope>
</dependency>

随意编写Controller

@Controller
public class RouterController {
    @GetMapping({"/","/index"})
    public String index(){
        return "index";
    }

    @GetMapping("/tologin")
    public String tologin(){
        return "views/login";
    }

    @GetMapping("/level1/{id}")
    public String level1(@PathVariable("id") int id){
        return "views/level1/"+id;
    }

    @GetMapping("/level2/{id}")
    public String level2(@PathVariable("id") int id){
        return "views/level2/"+id;
    }

    @GetMapping("/level3/{id}")
    public String level3(@PathVariable("id") int id){
        return "views/level3/"+id;
    }
}

默认情况下,引入了SpringSecurity,所有的页面都需要进行认证

编写配置类,修改默认配置

//@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter  {

    //这里可以配置一些忽略拦截项 当然也可以下面走匿名访问 不建议
    @Override
    public void configure(WebSecurity web) throws Exception {
        //静态资源过滤
        web.ignoring().antMatchers("/resources/**");
        //过滤某个路由
        web.ignoring().antMatchers("/vercode");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            	//为不同的访问路径配置不同的权限
                .antMatchers("/level1/**").hasAnyRole("vip1","vip2","vip3")
                .antMatchers("/level2/**").hasAnyRole("vip2","vip3")
                .antMatchers("/level3/**").hasAnyRole("vip3")
            	//剩下的路径设置为所有人皆可访问
                .anyRequest().permitAll();
        
        //配置登录相关
        http.formLogin()
            //登录页所在路由 默认/login
            .loginPage("/tologin")
            //登录表单发送目标的地址 如果不配置 默认是你的登录页所在路由
            .loginProcessingUrl("/login")
            //表单中参数的名称 默认username password
            .usernameParameter("name")
            .passwordParameter("pwd")
            .successHandler(new AuthenticationSuccessHandler() {
                    @Override
                    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                        //判断请求是异步(返回JSON)还是同步(返回页面)
                        String xRequestedWith = request.getHeader("x-requested-with");//通过请求头判断
                        if("XMLHttpRequest".equals(xRequestedWith)){//异步返回
                            response.setContentType("application/plain;charset=utf-8");
                            PrintWriter writer = response.getWriter();
                            writer.write(CommunityUtil.getJSONString(403,"你还没有登录"));
                        }else {//同步返回
                            response.sendRedirect(request.getContextPath()+"/login");
                        }
                    }
                })
            .failureHandler(new AuthenticationFailureHandler() {
                    @Override
                    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                        //同上
                    }
                })
        
        //退出登录相关配置 差不多和上面一样 源码中有简易的教学Demo
        http.logout();
        
        //记住我功能,如果你使用的是自定义登录页面,需要自己写选择框
        http.rememberMe()
            //表单中参数的名称
            .rememberMeParameter("remember");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //添加账户,向内存中,一般用来测试用的,实际开发一般不这么玩
        //BCryptPasswordEncoder是SpringSecurity提供的密码编码工具,可以非常方便的时间密码的加密和加盐,相同明文加密的密码完全不同
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
                .withUser("saber").password(new BCryptPasswordEncoder().encode("123")).roles("vip1")
                .and()
                .withUser("root").password(new BCryptPasswordEncoder().encode("123")).roles("vip1","vip2","vip3");
    }
    //也可以通过配置文件配置
    //spring.security.user.name=javaboy
    //spring.security.user.password=123
}

到此为止,一个大体的使用框架就搭建完成了,可以进行测试了

更多使用

登录成功和失败处理

http.formLogin()
        .loginPage("/tologin")
        .loginProcessingUrl("/login")
        .usernameParameter("name")
        .passwordParameter("pwd")
    	//登录成功于失败的跳转路由
    	.successForwardUrl("/success")
        .failureForwardUrl("/failure");

一般上面这种做法无法满足我们的需求,而且查看源码可知,底层就是一个forward跳转,我们知道forward跳转是无法跳到应用之外的页面的,由于这些功能往往会有比较复杂的逻辑,所以SpringSecurity给我们提供了.successHandler()方法去自己实现一个成功跳转逻辑,需要给他一个实现了AuthenticationSuccessHandler接口的类,它会去执行里面的onAuthenticationSuccess()方法

http.formLogin()
              .loginPage("/tologin")
              .loginProcessingUrl("/login")
              .usernameParameter("name")
              .passwordParameter("pwd")
		//也可以使用简单的路径跳转,看需求 .successForwardUrl()
              .successHandler(new AuthenticationSuccessHandler() {
                  //这里直接在这里实例化一个接口,也可以先创建一个类继承接口在实现,然后通过new的形式或注入的形式在这里使用
                  @Override
                  public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
                      //判断请求是异步(返回JSON)还是同步(返回页面)
                      String xRequestedWith = httpServletRequest.getHeader("x-requested-with");//通过请求头判断
                      if("XMLHttpRequest".equals(xRequestedWith)){//异步返回
                          httpServletResponse.setContentType("application/plain;charset=utf-8");
                          PrintWriter writer = httpServletResponse.getWriter();
                          writer.write(CommunityUtil.getJSONString(403,"你还没有登录"));
                      }else {//同步返回
                          httpServletResponse.sendRedirect(httpServletRequest.getContextPath()+"/login");
                      }
                  }
              })
              .failureHandler(new AuthenticationFailureHandler() {
                  @Override
                  public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
                      
                  }
              });

上面采用了最简单的写法,可读性差,建议还是自己去继承并实现,然后通过new的方式或注入的方式提供给.successHandler()

权限不足处理

//权限不够时
     http.exceptionHandling()
             .authenticationEntryPoint(new AuthenticationEntryPoint() {
                 //没有登录时处理
                 @Override
                 public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
                     //判断请求是异步(返回JSON)还是同步(返回页面)
                     String xRequestedWith = request.getHeader("x-requested-with");//通过请求头判断
                     if("XMLHttpRequest".equals(xRequestedWith)){//异步返回
                         response.setContentType("application/plain;charset=utf-8");
                         PrintWriter writer = response.getWriter();
                         writer.write(CommunityUtil.getJSONString(403,"你还没有登录"));
                     }else {//同步返回
                         response.sendRedirect(request.getContextPath()+"/login");
                     }
                 }
             })
             .accessDeniedHandler(new AccessDeniedHandler() {
                 //权限不足时处理
                 @Override
                 public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
                     //判断请求是异步(返回JSON)还是同步(返回页面)
                     String xRequestedWith = request.getHeader("x-requested-with");//通过请求头判断
                     if("XMLHttpRequest".equals(xRequestedWith)){//异步返回
                         response.setContentType("application/plain;charset=utf-8");
                         PrintWriter writer = response.getWriter();
                         writer.write(CommunityUtil.getJSONString(403,"权限不足"));
                     }else {//同步返回
                         response.sendRedirect(request.getContextPath()+"/denied");
                     }
                 }
             });

自定义登录逻辑

自定义登录逻辑需要创建一个实现UserDetailsService接口的类,并把它注入到IOC容器中

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleMapper roleMapper;

    @Bean
    private BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        com.example.security.entity.User u = userMapper.findUserByName(s);
        System.out.println(u.toString());
        if (u == null) {
            throw new UsernameNotFoundException("用户名不存在!");
        }
        //比较密码,匹配成功会返回UserDetails,实际上也会去数据库查
        String password = passwordEncoder.encode(u.getPassword());

        //用于添加用户的权限。只要把用户权限添加到authorities。
        List<GrantedAuthority> authorities = new ArrayList<>();
        Role role = roleMapper.findRoleById((int) u.getRoleId());
        if (role != null) {
            //使用Role和Authority都是这一套代码
            //这里有给大坑 如果你使用Role进行授权的话,一定要如下加上 ROLE_
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName()));
        }

        User user = new User(u.getUsername(), password, authorities);
        return user;
    }
}

然后在配置类中添加如下(不是必须的,建议写上):

@Autowired
UserDetailsService userDetailsService;

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

认证授权注解

在使用注解前需要在配置类或启动类上添加@EnableGlobalMethodSecurity表示开启注解认证授权

@Secured

需要在上面添加的注解中添加一个参数@EnableGlobalMethodSecurity(securedEnabled = true),开启这个注解的使用

用户具有某个Role可以访问,在Controller中使用

@GetMapping("/level1")
@Secured({"ROLE_vip1","ROLE_vip2","ROLE_vip3"})
public String level1() {
    return "level1";
}

@GetMapping("/level2")
@Secured({"ROLE_vip2","ROLE_vip3"})
public String level2() {
    return "level2";
}

@GetMapping("/level3")
@Secured({"ROLE_vip3"})
public String level3() {
    return "level3";
}

@PreAuthorize

需要在上面添加的注解中添加一个参数@EnableGlobalMethodSecurity(prePostEnabled = true),开启这个注解的使用

进入方法执行之前进行验证

@GetMapping("/level3")
//@PreAuthorize("hasRole('ROLE_xxx')") 这里直接使用上面介绍过的方法名称 就根调用方法一样
    @PreAuthorize("hasAnyAuthority('admin3')")
public String level3() {
    return "level3";
}

@PostAuthorize

需要在上面添加的注解中添加一个参数@EnableGlobalMethodSecurity(prePostEnabled = true),开启这个注解的使用

在方法执行之后判断是否有权限

@GetMapping("/level3")
@PostAuthorize("hasAnyAuthority('admin3')")
public String level3() {
    System.out.println("fwcg");
    return "level3";
}

@PreFilter / @PostFilter

对传入 / 传出的数据进行过滤 Spring Security将移除使对应表达式的结果为false的元素。

@PostFilter("filterObject.id%2==0")
@GetMapping("/getuser")
public List<User> findAll() {
    List<User> userList = new ArrayList<User>();
    User user;
    for (int i = 0; i < 10; i++){
        user = new User();
        user.setId(i);
        userList.add(user);
    }
    System.out.println(userList);
    return userList;
}

上述代码表示将对返回结果中id不为偶数的user进行移除。filterObject是使用@PreFilter和@PostFilter时的一个内置表达式,表示集合中的当前对象

当@PreFilter标注的方法拥有多个集合类型的参数时,需要通过@PreFilter的filterTarget属性指定当前@PreFilter是针对哪个参数进行过滤的。

@PreFilter(filterTarget="ids", value="filterObject%2==0")
public void delete(List<Integer> ids, List<String> usernames) {
    ...
}

记住我

最简单的实现方式是储存在内存中,在配置类中添加如下代码即可

http.rememberMe();//实现记住我自动登录,核心的代码只有这这一行

如果你使用默认登录页,什么也不用做,但如果你是自定义的,请在页面中添加复选框,

<label> <input type="checkbox" name="remember-me" />记住我</label>

前端传值时SpringSecurity将读取键: remember-me,只能叫这个名,Ajax传递参数也必须为 remember-me,但后端可以通过.rememberMeParameter("xxx") 修改参数名称,还可以通过.rememberMeCookieName()修改Cookie中Key的名称

.tokenValiditySeconds(60*2)这个可以修改有效时长默认2周,单位秒

http.rememberMe()
    .rememberMeParameter("rm")
    .rememberMeCookieName("rm-cookie")
    .tokenValiditySeconds(60*2);

基于数据库的实现

全部操作在配置类中

先注入数据源

@Autowired
private DataSource dataSource;

注入TokenRepostory组件

@Bean
public PersistentTokenRepository persistentTokenRepository() {
    JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
    jdbcTokenRepository.setDataSource(dataSource);//使用数据源
    jdbcTokenRepository.setCreateTableOnStartup(true);//创建数据表,第一次运行的时候使用,以后注释吊,不然报错
    return jdbcTokenRepository;
}

配置

http.rememberMe()
    .rememberMeParameter("rm")
    .rememberMeCookieName("rm-cookie")
    .tokenRepository(persistentTokenRepository())
    .tokenValiditySeconds(60*2);
	//.userDetailsService(userDetailsService);//如果上面自定义登录中直接指示了auth.userDetailsService(userDetailsService);这里可以不写

这样即可

整合JWT

导入依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

创建JWT工具类

管理Token相关的操作

/**
 * jwt 工具类 主要是生成token 检查token等相关方法
 */
public class JwtUtils {

    public static final String TOKEN_HEADER = "Authorization";

    public static final String TOKEN_PREFIX = "Bearer ";

    // TOKEN 过期时间
    public static final long EXPIRATION = 1000 * 60 * 30; // 三十分钟

    public static final String APP_SECRET_KEY = "secret";

    private static final String ROLE_CLAIMS = "rol";

    /**
     * 生成token
     *
     * @param username
     * @param roles
     * @return
     */
    public static String createToken(String username, List<String> roles) {

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

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

    /**
     * 获取当前登录用户用户名
     *
     * @param token
     * @return
     */
    public static String getUsername(String token) {
        Claims claims = Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();
        return claims.get("username").toString();
    }

    /**
     * 获取当前登录用户角色
     *
     * @param token
     * @return
     */
    public static ArrayList<String> getUserRole(String token) {
        Claims claims = Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();
        ArrayList<String> rols = claims.get("rol", ArrayList.class);
        return rols;
    }

    /**
     * 获解析token中的信息
     *
     * @param token
     * @return
     */
    public static Claims checkJWT(String token) {
        try {
            final Claims claims = Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();
            return claims;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 检查token是否过期
     *
     * @param token
     * @return
     */
    public static boolean isExpiration(String token) {
        Claims claims = Jwts.parser().setSigningKey(APP_SECRET_KEY).parseClaimsJws(token).getBody();
        return claims.getExpiration().before(new Date());
    }
}

创建JwtUser类

主要用于封装登录用户相关信息,例如用户名,密码,权限集合等,必须实现UserDetails接口

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.Collections;

public class JwtUser implements UserDetails {

    private Integer id;
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> authorities;

    public JwtUser() {
    }

    // 写一个能直接使用user创建jwtUser的构造器
    public JwtUser(User user, Collection<? extends GrantedAuthority> authorities) {
        id = Math.toIntExact(user.getId());
        username = user.getUsername();
        password = user.getPassword();
        this.authorities = authorities;
    }

    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

    public String getPassword() {
        return password;
    }

    public String getUsername() {
        return username;
    }

    public boolean isAccountNonExpired() {
        return true;
    }

    public boolean isAccountNonLocked() {
        return true;
    }

    public boolean isCredentialsNonExpired() {
        return true;
    }

    public boolean isEnabled() {
        return true;
    }
}

创建JwtUserService

类似于自定义登录逻辑,必须实现UserDetailsService

@Service
public class JwtUserService implements UserDetailsService {

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Autowired
    private UserMapper userMapper;

    @Autowired
    private RoleMapper roleMapper;

    @Bean
    private BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 根据前端传入的用户信息 去数据库查询是否存在该用户
     *
     * @param s
     * @return
     * @throws UsernameNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = this.userMapper.findUserByName(s);

        user.setPassword(passwordEncoder.encode(user.getPassword()));

        //用于添加用户的权限。只要把用户权限添加到authorities。
        List<GrantedAuthority> authorities = new ArrayList<>();
        Role role = roleMapper.findRoleById((int) user.getRoleId());
        if (role != null) {
            authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName()));
        }

        if (user != null) {
            JwtUser jwtUser = new JwtUser(user, authorities);
            return jwtUser;
        } else {
            try {
                throw new ValidationException("该用户不存在");
            } catch (ValidationException e) {
                e.printStackTrace();
            }
        }
        return null;
    }
}

自定义用户登录拦截器

/**
 * 验证用户名密码正确后,生成一个token,并将token返回给客户端
 * 该类继承自UsernamePasswordAuthenticationFilter,重写了其中的2个方法 ,
 * attemptAuthentication:接收并解析用户凭证。
 * successfulAuthentication:用户成功登录后,这个方法会被调用,我们在这个方法里生成token并返回。
 */
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

    private AuthenticationManager authenticationManager;

    /**
     * security拦截默认是以POST形式走/login请求,我们这边设置为走/token请求
     *
     * @param authenticationManager
     */
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        super.setFilterProcessesUrl("/token");
    }

    /**
     * 接收并解析用户凭证
     *
     * @param request
     * @param response
     * @return
     * @throws AuthenticationException
     */
    @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());

        List<String> roles = new ArrayList<>();
        Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
        for (GrantedAuthority authority : authorities) {
            roles.add(authority.getAuthority());
        }

        String token = JwtUtils.createToken(jwtUser.getUsername(), roles);
        // 返回创建成功的token  但是这里创建的token只是单纯的token
        // 按照jwt的规定,最后请求的时候应该是 `Bearer token`
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        String tokenStr = JwtUtils.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());
    }
}

自定义权限拦截器

假如admin登录成功后,携带token去请求其他接口时,该拦截器会判断权限是否正确

/**
 * 登录成功之后走此类进行  鉴定 权限
 */
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(JwtUtils.TOKEN_HEADER);
        // 如果请求头中没有Authorization信息则直接放行了
        if (tokenHeader == null || !tokenHeader.startsWith(JwtUtils.TOKEN_PREFIX)) {
            chain.doFilter(request, response);
            return;
        }
        // 如果请求头中有token,则进行解析,并且设置认证信息
        try {
            SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
        } catch (Exception e) {
            e.printStackTrace();
        }
        super.doFilterInternal(request, response, chain);
    }

    // 这里从token中获取用户信息并新建一个token 就是上面说的设置认证信息
    private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) throws Exception {

        String token = tokenHeader.replace(JwtUtils.TOKEN_PREFIX, "");

        // 检测token是否过期 如果过期会自动抛出错误
        JwtUtils.isExpiration(token);
        String username = JwtUtils.getUsername(token);
        ArrayList<String> roles = JwtUtils.getUserRole(token);
        List<SimpleGrantedAuthority> authorities = new ArrayList<>();
        if (roles != null) {
            for (String role : roles) {
                authorities.add(new SimpleGrantedAuthority(role));
            }
        }
        if (username != null) {
            return new UsernamePasswordAuthenticationToken(username, null, authorities);
        }
        return null;
    }
}

SpringSecurity配置

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    JwtUserService jwtUserService;

    @Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
//        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }


    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers("/resources/**");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/level1/**").authenticated()//.hasAnyRole("vip1","vip2","vip3")
                .antMatchers("/level2/**").authenticated()//.hasAnyRole("vip2","vip3")
                .antMatchers("/level3/**").authenticated()//.hasAnyRole("vip3")
                .anyRequest().permitAll();

        http.addFilter(new JWTAuthenticationFilter(authenticationManager())) // 用户登录拦截
            .addFilter(new JWTAuthorizationFilter(authenticationManager())) // 权限拦截
            // 不需要session
            .sessionManagement()
            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .exceptionHandling();

        http.formLogin();

        http.csrf().disable();

        http.logout().logoutUrl("/logout").logoutSuccessUrl("/getuser");

        http.rememberMe()
                .rememberMeParameter("rm")
                .rememberMeCookieName("rm-cookie")
                .tokenRepository(persistentTokenRepository())
                .tokenValiditySeconds(60*2);
    }

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

参考:

官网:https://docs.spring.io/spring-security/site/docs/5.3.9.RELEASE/reference/html5/#servlet-applications

视频:https://www.bilibili.com/video/BV15a411A7kP

参考文章1:https://www.cnblogs.com/lenve/p/11242055.html

参考文章2:https://www.jianshu.com/p/7817e372c1db

参考文章3:https://blog.csdn.net/qq_42640067/article/details/113062222

参考文章4:https://blog.csdn.net/qq_22172133/article/details/86503223

JWT参考:https://blog.csdn.net/weixin_45452416/article/details/109528425