Java框架中常用的几种成熟的token生成框架对比

发布时间 2023-09-20 19:00:54作者: qd372502

Java框架中常用的几种成熟的token生成框架有:

  • Spring Security:一个基于Spring的安全框架,提供了声明式的安全访问控制解决方案,支持多种认证和授权机制,如OAuth2.0、JWT等。
  • Apache Shiro:一个轻量级的Java安全框架,提供了身份认证、授权、加密、会话管理等功能,支持多种数据源和缓存实现,易于集成和扩展。
  • Sa-Token:一个轻量级的Java权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权等一系列权限相关问题,使用简单,功能强大。

 

Java框架中常用的几种成熟的token生成框架对比

在Web开发中,权限认证是一个非常重要的功能,它可以保证系统的安全性和用户的体验。通常,我们会使用一些成熟的框架来实现权限认证,而不是自己从零开始开发。在Java框架中,常用的几种成熟的token生成框架有Spring Security、Apache Shiro和Sa-Token。这三个框架都有各自的特点和优势,也有一些共同点和差异。本文将从介绍、底层原理、使用实例、优缺点、适用场景等几个维度来对比这三个框架,希望能够给大家提供一些参考。

介绍

Spring Security

Spring Security是一个基于Spring的安全框架,提供了声明式的安全访问控制解决方案,支持多种认证和授权机制,如OAuth2.0、JWT等。Spring Security可以与Spring Boot、Spring MVC、Spring Data等其他Spring项目无缝集成,也可以与其他技术栈配合使用。Spring Security拥有强大的社区支持和丰富的文档资源,是目前最流行的Java安全框架之一。

Apache Shiro

Apache Shiro是一个轻量级的Java安全框架,提供了身份认证、授权、加密、会话管理等功能,支持多种数据源和缓存实现,易于集成和扩展。Apache Shiro可以与任何Java应用程序或框架协作,无论是Web应用还是非Web应用。Apache Shiro有着简洁的API设计和灵活的配置方式,让开发者可以快速上手和定制自己的安全策略。

Sa-Token

Sa-Token是一个轻量级的Java权限认证框架,主要解决登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权等一系列权限相关问题。Sa-Token使用简单,功能强大,只需一行代码就可以完成会话登录或校验登录状态。Sa-Token更适合于前后台分离架构,支持多种模式和场景的token生成和验证。Sa-Token是一个相对较新的框架,但已经获得了不少关注和好评。

底层原理

Spring Security

Spring Security的核心是一系列过滤器(Filter),它们组成了一个过滤器链(Filter Chain),负责拦截请求并进行安全处理。Spring Security提供了很多内置的过滤器,如UsernamePasswordAuthenticationFilter、BasicAuthenticationFilter、JwtAuthenticationFilter等,也允许开发者自定义过滤器并添加到过滤器链中。每个过滤器都有自己的职责和逻辑,例如验证用户身份、检查用户权限、刷新token等。当一个请求到达过滤器链时,它会按照顺序经过每个过滤器,并根据过滤器的返回结果决定是否继续执行下一个过滤器或者放行请求或者抛出异常。

Spring Security还提供了一些核心组件来支持过滤器链的工作,如AuthenticationManager、AuthenticationProvider、UserDetailsService、PasswordEncoder等。这些组件负责处理具体的认证和授权逻辑,如从数据库中查询用户信息、比对用户密码、生成token、解析token等。开发者可以根据自己的需求配置或替换这些组件,以实现不同的安全策略。

Apache Shiro

Apache Shiro的核心是一个安全管理器(SecurityManager),它是一个单例对象,负责管理所有与安全相关的操作,如创建和销毁主体(Subject)、执行认证和授权等。SecurityManager是一个抽象接口,有两个实现类:DefaultSecurityManager和WebSecurityManager。前者用于非Web应用,后者用于Web应用,并继承了前者的功能。

SecurityManager依赖于一些其他的组件来完成具体的安全任务,如Realm、SessionManager、CacheManager、Cryptography等。Realm是一个数据源组件,负责从数据库或其他地方获取用户信息和权限信息,并提供给SecurityManager进行认证和授权。SessionManager是一个会话管理组件,负责创建和维护主体的会话,可以支持多种会话存储方式,如内存、Cookie、缓存、数据库等。CacheManager是一个缓存管理组件,负责缓存一些频繁访问的数据,如用户信息、权限信息、会话信息等,以提高性能和减少数据库压力。Cryptography是一个加密组件,负责提供一些常用的加密算法,如MD5、SHA、AES等,用于对用户密码或敏感数据进行加密和解密。

Apache Shiro还提供了一些过滤器(Filter)来拦截Web请求,并根据配置的规则进行相应的安全处理,如重定向到登录页面、检查用户角色或权限、记住用户身份等。开发者可以通过简单的配置来启用或禁用这些过滤器,也可以自定义过滤器并添加到过滤器链中。

Sa-Token

Sa-Token的核心是一个静态工具类StpUtil,它提供了一系列静态方法来完成权限认证相关的操作,如登录、注销、校验登录状态、获取或设置会话属性、检查角色或权限等。StpUtil内部依赖于一个SaTokenManager对象,它是一个单例对象,负责管理Sa-Token的所有配置和组件。

SaTokenManager包含了一些核心组件来支持StpUtil的工作,如StpLogic、SaTokenDao、SaTokenAction等。StpLogic是一个核心逻辑组件,负责处理具体的认证和授权逻辑,如生成token、验证token、获取或设置会话数据等。StpLogic可以支持多种模式和场景的token生成和验证,如普通模式、Cookie模式、Password模式、OAuth2.0模式等。SaTokenDao是一个数据源组件,负责从数据库或其他地方读写会话数据,并提供给StpLogic进行处理。SaTokenDao可以支持多种数据源实现,如内存、Cookie、Redis等。SaTokenAction是一个行为逻辑组件,负责处理一些与Web请求相关的操作,如获取或设置Cookie、响应JSON数据、重定向到登录页面等。

Sa-Token还提供了一些注解(Annotation)来简化Web开发中的权限控制,如@SaCheckLogin、@SaCheckRole、@SaCheckPermission等。这些注解可以直接标注在Controller层的方法上,并根据注解的参数进行相应的安全检查,如校验是否登录、是否具有某个角色或权限等。如果检查不通过,则会抛出相应的异常,并由全局异常处理器进行处理。

使用实例

Spring Security

以下是一个使用Spring Security实现基于JWT的认证和授权的简单示例:

  1. 引入依赖

Spring Security

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>
  1. 配置Security
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

  @Autowired
  private UserDetailsService userDetailsService;

  @Autowired
  private JwtAuthenticationFilter jwtAuthenticationFilter;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    // 使用自定义的UserDetailsService和PasswordEncoder
    auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // 关闭csrf和session
    http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    // 配置登录和登出的url和逻辑
    http.formLogin().loginProcessingUrl("/login").successHandler(new LoginSuccessHandler()).failureHandler(new LoginFailureHandler());
    http.logout().logoutUrl("/logout").logoutSuccessHandler(new LogoutSuccessHandler());
    // 配置需要认证和授权的url和角色
    http.authorizeRequests()
      .antMatchers("/login", "/logout").permitAll()
      .antMatchers("/admin/**").hasRole("ADMIN")
      .antMatchers("/user/**").hasRole("USER")
      .anyRequest().authenticated();
    // 添加自定义的Jwt过滤器
    http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
  }

  @Bean
  @Override
  public AuthenticationManager authenticationManagerBean() throws Exception {
    // 重写此方法,以便在Controller层调用
    return super.authenticationManagerBean();
  }
}
  1. 实现UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

  @Autowired
  private UserRepository userRepository;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 根据用户名从数据库中查询用户信息,如果不存在则抛出异常
    User user = userRepository.findByUsername(username);
    if (user == null) {
      throw new UsernameNotFoundException("用户不存在");
    }
    // 返回一个UserDetails对象,包含用户名、密码、角色等信息
    return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthorities(user.getRoles()));
  }

  // 将用户的角色转换为GrantedAuthority对象的集合,用于授权判断
  private Collection<? extends GrantedAuthority> getAuthorities(List<String> roles) {
    return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList());
  }
}
  1. 实现JwtAuthenticationFilter
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

  @Autowired
  private JwtUtils jwtUtils;

  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
    // 获取请求头中的Authorization字段,即token
    String token = request.getHeader("Authorization");
    if (token != null && token.startsWith("Bearer ")) {
      // 去掉token前缀,解析token,获取用户名和角色信息
      token = token.substring(7);
      Claims claims = jwtUtils.parseToken(token);
      String username = claims.getSubject();
      List<String> roles = claims.get("roles", List.class);
      // 如果token有效,构造一个UsernamePasswordAuthenticationToken对象,设置到SecurityContext中,用于授权判断
      if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, null, getAuthorities(roles));
        authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
      }
    }
    // 继续执行下一个过滤器
    filterChain.doFilter(request, response);
  }

  // 将用户的角色转换为GrantedAuthority对象的集合,用于授权判断
  private Collection<? extends GrantedAuthority> getAuthorities(List<String> roles) {
    return roles.stream().map(role -> new SimpleGrantedAuthority("ROLE_" + role)).collect(Collectors.toList());
  }
}
  1. 实现JwtUtils
@Component
public class JwtUtils {

  // 签名密钥,可以从配置文件中读取
  private String secretKey = "secret";

  // 签名过期时间,可以从配置文件中读取
  private long expirationTime = 3600 * 1000;

  // 根据用户名和角色生成token
  public String generateToken(String username, List<String> roles) {
    // 设置token的过期时间
    Date expirationDate = new Date(System.currentTimeMillis() + expirationTime);
    // 创建一个JwtBuilder对象,设置token的用户名、角色、过期时间、签名密钥等信息
    JwtBuilder builder = Jwts.builder()
      .setSubject(username)
      .claim("roles", roles)
      .setExpiration(expirationDate)
      .signWith(SignatureAlgorithm.HS256, secretKey);
    // 返回生成的token字符串
    return builder.compact();
  }

  // 解析token,获取Claims对象,包含用户名、角色、过期时间等信息
  public Claims parseToken(String token) {
    // 使用JwtParser对象,设置签名密钥,解析token,返回Claims对象
    JwtParser parser = Jwts.parser().setSigningKey(secretKey);
    return parser.parseClaimsJws(token).getBody();
  }

  // 判断token是否过期
  public boolean isExpired(String token) {
    // 获取token的过期时间,与当前时间比较,返回结果
    Date expirationDate = parseToken(token).getExpiration();
    return expirationDate.before(new Date());
  }

  // 刷新token,生成一个新的token,延长过期时间
  public String refreshToken(String token) {
    // 获取原来的token中的用户名和角色信息
    Claims claims = parseToken(token);
    String username = claims.getSubject();
    List<String> roles = claims.get("roles", List.class);
    // 调用生成token的方法,返回新的token
    return generateToken(username, roles);
  }
}
  1. 实现Controller
@RestController
public class UserController {

  @Autowired
  private AuthenticationManager authenticationManager;

  @Autowired
  private JwtUtils jwtUtils;

  // 登录接口,参数为用户名和密码,返回一个包含token的JSON数据
  @PostMapping("/login")
  public String login(@RequestParam String username, @RequestParam String password) {
    // 使用用户名和密码创建一个UsernamePasswordAuthenticationToken对象,用于认证
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(username, password);
    try {
      // 调用AuthenticationManager的authenticate方法进行认证,如果成功,则返回一个包含用户信息和权限信息的Authentication对象
      Authentication authentication = authenticationManager.authenticate(authenticationToken);
      // 获取用户信息和权限信息,转换为UserDetails对象
      UserDetails userDetails = (UserDetails) authentication.getPrincipal();
      // 获取用户的角色列表,去掉前缀"ROLE_"
      List<String> roles = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).map(role -> role.substring(5)).collect(Collectors.toList());
      // 调用JwtUtils的generateToken方法,根据用户名和角色列表生成token
      String token = jwtUtils.generateToken(userDetails.getUsername(), roles);
      // 返回一个包含token的JSON数据
      return "{\"token\": \"" + token + "\"}";
    } catch (AuthenticationException e) {
      // 如果认证失败,则抛出异常或返回错误信息
      throw e;
      // return "{\"error\": \"用户名或密码错误\"}";
    }
  }

  // 注销接口,无需参数,返回一个成功信息
  @GetMapping("/logout")
  public String logout() {
    // 获取当前登录的用户信息,如果存在,则注销登录,清除SecurityContext中的Authentication对象
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    if (authentication != null) {
      SecurityContextHolder.clearContext();
    }
    // 返回一个成功信息
    return "{\"message\": \"注销成功\"}";
  }

  // 测试接口,需要ADMIN角色才能访问,返回一个欢迎信息
  @GetMapping("/admin/hello")
  public String adminHello() {
    return "{\"message\": \"Hello, admin\"}";
  }

  // 测试接口,需要USER角色才能访问,返回一个欢迎信息
  @GetMapping("/user/hello")
  public String userHello() {
    return "{\"message\": \"Hello, user\"}";
  }
}

Apache Shiro

以下是一个使用Apache Shiro实现基于JWT的认证和授权的简单示例:

  1. 引入依赖

Apache Shiro

<dependency>
  <groupId>org.apache.shiro</groupId>
  <artifactId>shiro-spring-boot-web-starter</artifactId>
  <version>1.7.1</version>
</dependency>
<dependency>
  <groupId>io.jsonwebtoken</groupId>
  <artifactId>jjwt</artifactId>
  <version>0.9.1</version>
</dependency>
  1. 配置Shiro
@Configuration
public class ShiroConfig {

  @Autowired
  private UserRealm userRealm;

  @Autowired
  private JwtFilter jwtFilter;

  @Bean
  public SecurityManager securityManager() {
    // 创建一个DefaultWebSecurityManager对象,设置自定义的UserRealm和SessionManager
    DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
    securityManager.setRealm(userRealm);
    securityManager.setSessionManager(sessionManager());
    return securityManager;
  }

  @Bean
  public SessionManager sessionManager() {
    // 创建一个DefaultWebSessionManager对象,禁用Cookie和Session的创建和使用
    DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
    sessionManager.setSessionIdCookieEnabled(false);
    sessionManager.setSessionIdUrlRewritingEnabled(false);
    return sessionManager;
  }

  @Bean
  public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) {
    // 创建一个ShiroFilterFactoryBean对象,设置SecurityManager和过滤器链
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    // 配置登录和登出的url和逻辑
    shiroFilterFactoryBean.setLoginUrl("/login");
    shiroFilterFactoryBean.setSuccessUrl("/index");
    shiroFilterFactoryBean.setUnauthorizedUrl("/unauthorized");
    // 配置需要认证和授权的url和角色
    Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
    filterChainDefinitionMap.put("/login", "anon");
    filterChainDefinitionMap.put("/logout", "logout");
    filterChainDefinitionMap.put("/admin/**", "roles[ADMIN]");
    filterChainDefinitionMap.put("/user/**", "roles[USER]");
    filterChainDefinitionMap.put("/**", "authc");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
    // 添加自定义的Jwt过滤器,并设置到过滤器链中,拦截所有需要认证的请求
    Map<String, Filter> filters = new HashMap<>();
    filters.put("authc", jwtFilter);
    shiroFilterFactoryBean.setFilters(filters);
    return shiroFilterFactoryBean;
  }
}
  1. 实现UserRealm
@Component
public class UserRealm extends AuthorizingRealm {

  @Autowired
  private UserRepository userRepository;

  @Autowired
  private JwtUtils jwtUtils;

  // 设置此Realm支持的Token类型为JwtToken
  @Override
  public boolean supports(AuthenticationToken token) {
    return token instanceof JwtToken;
  }

  // 执行授权逻辑,根据用户的角色信息,赋予相应的权限
  @Override
  protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
    // 获取用户的角色列表,转换为Set集合
    List<String> roles = (List<String>) principals.getPrimaryPrincipal();
    Set<String> roleSet = new HashSet<>(roles);
    // 创建一个SimpleAuthorizationInfo对象,设置用户的角色信息,返回结果
    SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
    info.setRoles(roleSet);
    return info;
  }

  // 执行认证逻辑,根据token中的用户名,查询用户信息,并进行密码比对和token有效性检查
  @Override
  protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    // 获取token字符串,转换为JwtToken对象
    String jwtToken = (String) token.getCredentials();
    JwtToken jwt = new JwtToken(jwtToken);
    // 解析token,获取用户名和角色信息
    Claims claims = jwtUtils.parseToken(jwtToken);
    String username = claims.getSubject();
    List<String> roles = claims.get("roles", List.class);
    // 根据用户名从数据库中查询用户信息,如果不存在则抛出异常
    User user = userRepository.findByUsername(username);
    if (user == null) {
      throw new UnknownAccountException("用户不存在");
    }
    // 比对用户密码,如果不匹配则抛出异常
    if (!user.getPassword().equals(claims.get("password"))) {
      throw new IncorrectCredentialsException("密码错误");
    }
    // 判断token是否过期,如果过期则抛出异常
    if (jwtUtils.isExpired(jwtToken)) {
      throw new ExpiredCredentialsException("token已过期");
    }
    // 创建一个SimpleAuthenticationInfo对象,设置用户的角色信息作为主体,设置token作为凭证,返回结果
    return new SimpleAuthenticationInfo(roles, jwt, getName());
  }
}
  1. 实现JwtFilter
@Component
public class JwtFilter extends AuthenticatingFilter {

  @Autowired
  private JwtUtils jwtUtils;

  // 创建一个JwtToken对象,用于执行认证逻辑
  @Override
  protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) throws Exception {
    // 获取请求头中的Authorization字段,即token
    String token = ((HttpServletRequest) request).getHeader("Authorization");
    if (token == null || !token.startsWith("Bearer ")) {
      return null;
    }
    // 去掉token前缀,返回一个JwtToken对象
    token = token.substring(7);
    return new JwtToken(token);
  }

  // 判断是否允许访问,如果带有有效的token,则允许访问
  @Override
  protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    // 判断请求是否带有token
    if (createToken(request, response) == null) {
      return false;
    }
    try {
      // 执行父类的登录方法,调用UserRealm的认证逻辑,如果没有抛出异常,则表示登录成功,允许访问
      executeLogin(request, response);
      return true;
    } catch (Exception e) {
      // 如果抛出异常,则表示登录失败,不允许访问
      return false;
    }
  }

  // 判断是否拒绝访问,如果没有带有有效的token,则拒绝访问,并返回错误信息
  @Override
  protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
    // 设置响应头和编码格式
    response.setContentType("application/json;charset=utf-8");
    response.setCharacterEncoding("UTF-8");
    // 获取输出流,输出错误信息
    PrintWriter out = response.getWriter();
    out.println("{\"error\": \"无效的token\"}");
    out.flush();
    out.close();
    return false;
  }
}
  1. 实现JwtUtils
@Component
public class JwtUtils {

  // 签名密钥,可以从配置文件中读取
  private String secretKey = "secret";

  // 签名过期时间,可以从配置文件中读取
  private long expirationTime = 3600 * 1000;

  // 根据用户名和密码和角色生成token
  public String generateToken(String username, String password, List<String> roles) {
    // 设置token的过期时间
    Date expirationDate = new Date(System.currentTimeMillis() + expirationTime);
    // 创建一个JwtBuilder对象,设置token的用户名、密码、角色、过期时间、签名密钥等信息
    JwtBuilder builder = Jwts.builder()
      .setSubject(username)
      .claim("password", password)
      .claim("roles", roles)
      .setExpiration(expirationDate)
      .signWith(SignatureAlgorithm.HS256, secretKey);
    // 返回生成的token字符串
    return builder.compact();
  }

  // 解析token,获取Claims对象,包含用户名、密码、角色、过期时间等信息
  public Claims parseToken(String token) {
    // 使用JwtParser对象,设置签名密钥,解析token,返回Claims对象
    JwtParser parser = Jwts.parser().setSigningKey(secretKey);
    return parser.parseClaimsJws(token).getBody();
  }

  // 判断token是否过期
  public boolean isExpired(String token) {
    // 获取token的过期时间,与当前时间比较,返回结果
    Date expirationDate = parseToken(token).getExpiration();
    return expirationDate.before(new Date());
  }

  // 刷新token,生成一个新的token,延长过期时间
  public String refreshToken(String token) {
    // 获取原来的token中的用户名、密码和角色信息
    Claims claims = parseToken(token);
    String username = claims.getSubject();
    String password = claims.get("password", String.class);
    List<String> roles = claims.get("roles", List.class);
    // 调用生成token的方法,返回新的token
    return generateToken(username, password, roles);
  }
}

 

 

  1. 实现Controller
@RestController
public class UserController {

  @Autowired
  private UserRepository userRepository;

  @Autowired
  private JwtUtils jwtUtils;

  // 登录接口,参数为用户名和密码,返回一个包含token的JSON数据
  @PostMapping("/login")
  public String login(@RequestParam String username, @RequestParam String password) {
    // 根据用户名从数据库中查询用户信息,如果不存在则返回错误信息
    User user = userRepository.findByUsername(username);
    if (user == null) {
      return "{\"error\": \"用户不存在\"}";
    }
    // 比对用户密码,如果不匹配则返回错误信息
    if (!user.getPassword().equals(password)) {
      return "{\"error\": \"密码错误\"}";
    }
    // 调用JwtUtils的generateToken方法,根据用户名、密码和角色列表生成token
    String token = jwtUtils.generateToken(user.getUsername(), user.getPassword(), user.getRoles());
    // 返回一个包含token的JSON数据
    return "{\"token\": \"" + token + "\"}";
  }

  // 注销接口,无需参数,返回一个成功信息
  @GetMapping("/logout")
  public String logout() {
    // 获取当前登录的用户信息,如果存在,则注销登录,清除Subject对象
    Subject subject = SecurityUtils.getSubject();
    if (subject.isAuthenticated()) {
      subject.logout();
    }
    // 返回一个成功信息
    return "{\"message\": \"注销成功\"}";
  }

  // 测试接口,需要ADMIN角色才能访问,返回一个欢迎信息
  @GetMapping("/admin/hello")
  public String adminHello() {
    return "{\"message\": \"Hello, admin\"}";
  }

  // 测试接口,需要USER角色才能访问,返回一个欢迎信息
  @GetMapping("/user/hello")
  public String userHello() {
    return "{\"message\": \"Hello, user\"}";
  }
}

Sa-Token

以下是一个使用Sa-Token实现基于JWT的认证和授权的简单示例:

  1. 引入依赖

Sa-Token

<dependency>
  <groupId>cn.dev33</groupId>
  <artifactId>sa-token-spring-boot-starter</artifactId>
  <version>1.28.0</version>
</dependency>
  1. 配置Sa-Token
@Configuration
public class SaTokenConfig {

  @Autowired
  private UserRealm userRealm;

  @Bean
  public SaTokenManager saTokenManager() {
    // 创建一个SaTokenManager对象,设置自定义的UserRealm和一些配置参数
    SaTokenManager saTokenManager = new SaTokenManager();
    saTokenManager.setSaTokenRealm(userRealm);
    saTokenManager.setIsV(true); // 开启token的校验模式
    saTokenManager.setTokenStyle(SaTokenConsts.TOKEN_STYLE_JWT); // 设置token的风格为JWT
    saTokenManager.setTimeout(3600); // 设置token的过期时间为3600秒
    saTokenManager.setSecretKey("secret"); // 设置token的签名密钥
    return saTokenManager;
  }
}
  1. 实现UserRealm
@Component
public class UserRealm implements SaTokenRealm {

  @Autowired
  private UserRepository userRepository;

  // 执行登录逻辑,根据用户名和密码,查询用户信息,并返回一个包含用户id和角色列表的LoginModel对象
  @Override
  public LoginModel doLogin(String username, String password) {
    // 根据用户名从数据库中查询用户信息,如果不存在则抛出异常
    User user = userRepository.findByUsername(username);
    if (user == null) {
      throw new NotLoginException("用户不存在");
    }
    // 比对用户密码,如果不匹配则抛出异常
    if (!user.getPassword().equals(password)) {
      throw new NotLoginException("密码错误");
    }
    // 创建一个LoginModel对象,设置用户的id和角色列表,返回结果
    LoginModel loginModel = new LoginModel();
    loginModel.setId(user.getId());
    loginModel.setRoleList(user.getRoles());
    return loginModel;
  }

  // 根据用户id,查询用户信息,并返回一个包含用户id和角色列表的LoginModel对象
  @Override
  public LoginModel getLoginModel(Object loginId) {
    // 根据用户id从数据库中查询用户信息,如果不存在则抛出异常
    User user = userRepository.findById((Long) loginId);
    if (user == null) {
      throw new NotLoginException("用户不存在");
    }
    // 创建一个LoginModel对象,设置用户的id和角色列表,返回结果
    LoginModel loginModel = new LoginModel();
    loginModel.setId(user.getId());
    loginModel.setRoleList(user.getRoles());
    return loginModel;
  }
}
  1. 实现Controller
@RestController
public class UserController {

  // 登录接口,参数为用户名和密码,返回一个包含token的JSON数据
  @PostMapping("/login")
  public String login(@RequestParam String username, @RequestParam String password) {
    // 调用StpUtil的login方法,传入用户名和密码,执行登录逻辑,如果成功,则返回一个包含token的JSON数据
    StpUtil.login(username, password);
    return "{\"token\": \"" + StpUtil.getTokenValue() + "\"}";
  }

  // 注销接口,无需参数,返回一个成功信息
  @GetMapping("/logout")
  public String logout() {
    // 调用StpUtil的logout方法,执行注销逻辑,返回一个成功信息
    StpUtil.logout();
    return "{\"message\": \"注销成功\"}";
  }

  // 测试接口,需要ADMIN角色才能访问,返回一个欢迎信息
  @GetMapping("/admin/hello")
  @SaCheckRole("ADMIN")
  public String adminHello() {
    return "{\"message\": \"Hello, admin\"}";
  }

  // 测试接口,需要USER角色才能访问,返回一个欢迎信息
  @GetMapping("/user/hello")
  @SaCheckRole("USER")
  public String userHello() {
    return "{\"message\": \"Hello, user\"}";
  }
}

优缺点

Spring Security

优点

  • 功能强大,支持多种认证和授权机制,如OAuth2.0、JWT、LDAP等。
  • 集成方便,可以与Spring Boot、Spring MVC、Spring Data等其他Spring项目无缝集成,也可以与其他技术栈配合使用。
  • 社区活跃,拥有强大的社区支持和丰富的文档资源,遇到问题容易找到解决方案。

缺点

  • 学习成本高,配置复杂,需要理解Spring Security的架构和原理,才能灵活地使用和定制。
  • 侵入性强,与Spring框架耦合度高,不适合非Spring项目使用。
  • 性能开销大,过滤器链的执行和安全组件的调用会增加系统的响应时间和资源消耗。

Apache Shiro

优点

  • 轻量级,功能简洁,只提供了核心的安全功能,如认证、授权、加密、会话管理等。
  • 易于使用,API设计清晰,配置方式灵活,可以快速上手和定制自己的安全策略。
  • 通用性强,可以与任何Java应用程序或框架协作,无论是Web应用还是非Web应用。

缺点

  • 功能有限,不支持一些高级的安全机制,如OAuth2.0、JWT等,需要自己实现或集成第三方库。
  • 社区不活跃,更新速度慢,文档资源少,遇到问题难以寻求帮助。
  • 性能开销大,Realm的查询和缓存的维护会增加系统的响应时间和资源消耗。

Sa-Token

优点

  • 轻量级,功能强大,支持多种模式和场景的token生成和验证,如普通模式、Cookie模式、Password模式、OAuth2.0模式等。
  • 易于使用,API设计简洁,只需一行代码就可以完成会话登录或校验登录状态。
  • 适合前后台分离架构,支持多种数据源实现,如内存、Cookie、Redis等。

缺点

  • 功能不成熟,不支持一些常用的安全功能,如加密、会话管理等。
  • 社区不活跃,更新速度慢,文档资源少,遇到问题难以寻求帮助。
  • 侵入性强,与Spring框架耦合度高,不适合非Spring项目使用。

适用场景

Spring Security

  • 如果你的项目是基于Spring框架开发的,并且需要实现一些高级的安全机制,如OAuth2.0、JWT等,则可以选择Spring Security作为你的安全框架。
  • 如果你的项目是一个大型的企业级应用,并且需要一个完整的安全解决方案,则可以选择Spring Security作为你的安全框架。

Apache Shiro

  • 如果你的项目是一个中小型的应用,并且只需要实现一些核心的安全功能,如认证、授权、加密、会话管理等,则可以选择Apache Shiro作为你的安全框架。
  • 如果你的项目是一个非Web应用,并且需要一个通用的安全框架,则可以选择Apache Shiro作为你的安全框架。

Sa-Token

  • 如果你的项目是一个前后台分离架构的应用,并且需要一个轻量级且功能强大的token生成和验证框架,则可以选择Sa-Token作为你的安全框架。
  • 如果你的项目是一个新兴的应用,并且需要一个易于使用且适应多种场景的token生成和验证框架,则可以选择Sa-Token作为你的安全框架。

总结

本文对比了Java框架中常用的几种成熟的token生成框架,分别是Spring Security、Apache Shiro和Sa-Token。这三个框架都有各自的特点和优势,也有一些共同点和差异。开发者可以根据自己的项目需求和喜好,选择合适的框架来实现权限认证功能。当然,这三个框架都不是完美的,也有一些不足之处,需要不断地学习和改进。希望本文能够给大家提供一些参考和启发。感谢阅读!