SpringBoot + SpringSecurity + Mybatis-Plus + JWT实现分布式系统认证和授权

1. 简介#

  Spring Security是一个功能强大且易于扩展的安全框架,主要用于为Java程序提供用户认证(Authentication)和用户授权(Authorization)功能。
  用户认证指的是验证某个用户是否合法,即验证用户名密码是否正确;用户授权指的是验证用户是否拥有访问资源的权限。在一个系统中,用户认证和授权是分不开的,既要保证用户能合法登录系统,也要保住用户再访问资源时具有足够的权限。
  JWT(Json Web Token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。一般用于在身份提供者和服务提供者之间传递被认证成功的用户身份信息,以便于从资源服务器获取资源。基于无状态、结构简单、传输快且不会在服务端保存会话信息等的特点,在分布式系统认证授权场景中发挥重要作用。
  本文使用SpringBoot整合SpringSecurity实现JWT Token认证授权。

2. 数据库设计#

表名称 字段 说明
sys_user id(ID,主键)
username(用户名)
password(密码)
status(状态,0-正常,1-删除,2-禁用)
系统用户表
sys_role id(ID,主键)
role_name(角色名称)
系统角色表
sys_auth id(ID,主键)
name(权限名称)
permission(权限标识)
系统权限表
sys_user_role id(ID,主键)
user_id(用户ID)
role_id(角色ID)
系统用户角色中间表
sys_role_auth id(ID,主键)
role_id(角色ID)
auth_id(角色权限ID)
系统角色权限中间表

3. 搭建环境#

  • 创建项目
  • 修改pom.xml
Copy
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.c3stones</groupId> <artifactId>spring-security-jwt-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>spring-security-jwt-demo</name> <description>Spring Boot + Srping Security + Mybatis-Plus + JWT Demo</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.6.RELEASE</version> </parent> <properties> <java.version>1.8</java.version> <jjwt.version>0.9.0</jjwt.version> <druid.version>1.1.6</druid.version> <jwt.version>1.0.9.RELEASE</jwt.version> <fastjson.version>1.2.45</fastjson.version> <mybatis-plus.version>3.3.1</mybatis-plus.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!--Spring Security依赖 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!-- Mybatis-Plus 依赖 --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>${mybatis-plus.version}</version> </dependency> <!-- Druid 连接池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>${druid.version}</version> </dependency> <!-- StringUtils 工具 --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> </dependency> <!-- JSON工具 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>${fastjson.version}</version> </dependency> <!-- JWT依赖 --> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>${jwt.version}</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>${jjwt.version}</version> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
  • 添加配置文件application.yml
Copy
server: port: 8080 spring: datasource: type: com.alibaba.druid.pool.DruidDataSource driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/security?useSSL=false&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull username: root password: 123456 # JWT配置 jwt: # 密匙Key secret: JWTSecret,C3Stones # HeaderKey tokenHeader: Authorization # Token前缀 tokenPrefix: Bearer # 过期时间,单位秒 expiration: 86400 # 配置白名单(不需要认证) antMatchers: /login/**,/register/**,/static/** # Mybatis-plus配置 mybatis-plus: mapper-locations: classpath:mapper/*.xml global-config: db-config: id-type: AUTO configuration: # 打印sql log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
  • 添加Entity、Dao、Dao.xml、Serivce
      以系统用户SysUser为例,其余请自行添加。
      创建实体类:
Copy
import java.io.Serializable; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.Data; /** * 系统用户 * * @author CL * */ @Data @TableName("sys_user") public class SysUser implements Serializable { private static final long serialVersionUID = 1L; /** * 用户ID */ @TableId private Long id; /** * 用户名 */ private String username; /** * 密码 */ private String password; /** * 状态(0-正常,1-删除,2-禁用) */ private String status; }

  创建Dao类:

Copy
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.c3stones.entity.SysUser; /** * 系统用户Dao * * @author CL * */ @Mapper public interface SysUserDao extends BaseMapper<SysUser> { }

  在rsource目录下创建mapper文件夹,并在文件夹下创建SysUserDao.xml:

Copy
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.c3stones.dao.SysUserDao"> </mapper>

  创建Service:

Copy
import com.baomidou.mybatisplus.extension.service.IService; import com.c3stones.entity.SysUser; /** * 系统用户Service * * @author CL * */ public interface SysUserService extends IService<SysUser> { }

  创建Service实现类:

Copy
import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.c3stones.dao.SysUserDao; import com.c3stones.entity.SysUser; import com.c3stones.service.SysUserService; /** * 系统用户Service实现 * * @author CL * */ @Service public class SysUserSerivceImpl extends ServiceImpl<SysUserDao, SysUser> implements SysUserService { }
  • 创建系统用户详情类,必须实现org.springframework.security.core.userdetails.UserDetails:
Copy
import java.io.Serializable; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import com.c3stones.entity.SysUser; import lombok.Data; import lombok.EqualsAndHashCode; /** * 系统用户详情 * * @author CL * */ @Data @EqualsAndHashCode(callSuper = false) public class SysUserDetails extends SysUser implements UserDetails, Serializable { private static final long serialVersionUID = 1L; /** * 用户角色 */ private Collection<GrantedAuthority> authorities; /** * 账号是否过期 */ private boolean isAccountNonExpired = false; /** * 账号是否锁定 */ private boolean isAccountNonLocked = false; /** * 证书是否过期 */ private boolean isCredentialsNonExpired = false; /** * 账号是否有效 */ private boolean isEnabled = true; /** * 获得用户权限 */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } /** * 判断账号是否过期 */ @Override public boolean isAccountNonExpired() { return isAccountNonExpired; } /** * 判断账号是否锁定 */ @Override public boolean isAccountNonLocked() { return isAccountNonLocked; } /** * 判断证书是否过期 */ @Override public boolean isCredentialsNonExpired() { return isCredentialsNonExpired; } /** * 判断账号是否有效 */ @Override public boolean isEnabled() { return isEnabled; } }
  • 编写JWT配置信息类:
Copy
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * JWT配置基础类 * * @author CL * */ @Component @ConfigurationProperties(prefix = "jwt") @SuppressWarnings("static-access") public class JWTConfig { /** * 密匙Key */ public static String secret; /** * HeaderKey */ public static String tokenHeader; /** * Token前缀 */ public static String tokenPrefix; /** * 过期时间 */ public static Integer expiration; /** * 配置白名单 */ public static String antMatchers; /** * 将过期时间单位换算成毫秒 * * @param expiration 过期时间,单位秒 */ public void setExpiration(Integer expiration) { this.expiration = expiration * 1000; } public void setSecret(String secret) { this.secret = secret; } public void setTokenHeader(String tokenHeader) { this.tokenHeader = tokenHeader; } public void setTokenPrefix(String tokenPrefix) { this.tokenPrefix = tokenPrefix + " "; } public void setAntMatchers(String antMatchers) { this.antMatchers = antMatchers; } }

4. 编写工具类#

  • 创建响应工具类
Copy
import java.io.PrintWriter; import javax.servlet.ServletResponse; import com.alibaba.fastjson.JSON; import lombok.AllArgsConstructor; import lombok.Data; import lombok.extern.slf4j.Slf4j; /** * 响应结果工具类 * * @author CL * */ @Slf4j @Data @AllArgsConstructor public class ResponseUtils { /** * 返回编码 */ private Integer code; /** * 返回消息 */ private String msg; /** * 返回数据 */ private Object data; /** * Response输出Json格式 * * @param response * @param data 返回数据 */ public static void responseJson(ServletResponse response, Object data) { PrintWriter out = null; try { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); out = response.getWriter(); out.println(JSON.toJSONString(data)); out.flush(); } catch (Exception e) { log.error("Response输出Json异常:" + e); } finally { if (out != null) { out.close(); } } } /** * 返回信息 * * @param code 返回编码 * @param msg 返回消息 * @param data 返回数据 * @return */ public static ResponseUtils response(Integer code, String msg, Object data) { return new ResponseUtils(code, msg, data); } /** * 返回成功 * * @param data 返回数据 * @return */ public static ResponseUtils success(Object data) { return ResponseUtils.response(200, "成功", data); } /** * 返回失败 * * @param data 返回数据 * @return */ public static ResponseUtils fail(Object data) { return ResponseUtils.response(500, "失败", data); } }
  • 编写JWTToken工具类:
Copy
/** * JWT生产Token工具类 * * @author CL * */ @Slf4j public class JWTTokenUtil { /** * 创建Token * * @param sysUserDetails 用户信息 * @return */ public static String createAccessToken(SysUserDetails sysUserDetails) { String token = Jwts.builder().setId(// 设置JWT sysUserDetails.getId().toString()) // 用户Id .setSubject(sysUserDetails.getUsername()) // 主题 .setIssuedAt(new Date()) // 签发时间 .setIssuer("C3Stones") // 签发者 .setExpiration(new Date(System.currentTimeMillis() + JWTConfig.expiration)) // 过期时间 .signWith(SignatureAlgorithm.HS512, JWTConfig.secret) // 签名算法、密钥 .claim("authorities", JSON.toJSONString(sysUserDetails.getAuthorities())).compact(); // 自定义其他属性,如用户组织机构ID,用户所拥有的角色,用户权限信息等 return JWTConfig.tokenPrefix + token; } /** * 解析Token * * @param token Token信息 * @return */ public static SysUserDetails parseAccessToken(String token) { SysUserDetails sysUserDetails = null; if (StringUtils.isNotEmpty(token)) { try { // 去除JWT前缀 token = token.substring(JWTConfig.tokenPrefix.length()); // 解析Token Claims claims = Jwts.parser().setSigningKey(JWTConfig.secret).parseClaimsJws(token).getBody(); // 获取用户信息 sysUserDetails = new SysUserDetails(); sysUserDetails.setId(Long.parseLong(claims.getId())); sysUserDetails.setUsername(claims.getSubject()); // 获取角色 Set<GrantedAuthority> authorities = new HashSet<GrantedAuthority>(); String authority = claims.get("authorities").toString(); if (StringUtils.isNotEmpty(authority)) { List<Map<String, String>> authorityList = JSON.parseObject(authority, new TypeReference<List<Map<String, String>>>() { }); for (Map<String, String> role : authorityList) { if (!role.isEmpty()) { authorities.add(new SimpleGrantedAuthority(role.get("authority"))); } } } sysUserDetails.setAuthorities(authorities); } catch (Exception e) { log.error("解析Token异常:" + e); } } return sysUserDetails; } }

5. 编写Spring Security核心实现类#

  • 编写无权限处理类
Copy
import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import com.c3stones.utils.ResponseUtils; /** * 无权限处理类 * * @author CL * */ @Component public class UserAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { ResponseUtils.responseJson(response, ResponseUtils.response(403, "拒绝访问", accessDeniedException.getMessage())); } }
  • 编写未登录处理类
Copy
import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import com.c3stones.utils.ResponseUtils; /** * 未登录处理类 * * @author CL * */ @Component public class UserNotLoginHandler implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { ResponseUtils.responseJson(response, ResponseUtils.response(401, "未登录", authException.getMessage())); } }
  • 编写登录成功处理类
Copy
import java.util.HashMap; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import com.c3stones.security.entity.SysUserDetails; import com.c3stones.security.utils.JWTTokenUtil; import com.c3stones.utils.ResponseUtils; /** * 登录成功处理类 * * @author CL * */ @Component public class UserLoginSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { SysUserDetails sysUserDetails = (SysUserDetails) authentication.getPrincipal(); String token = JWTTokenUtil.createAccessToken(sysUserDetails); Map<String, String> tokenMap = new HashMap<>(); tokenMap.put("token", token); ResponseUtils.responseJson(response, ResponseUtils.response(200, "登录成功", tokenMap)); } }
  • 编写登录失败处理类
Copy
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.stereotype.Component; import com.c3stones.utils.ResponseUtils; /** * 登录失败处理类 * * @author CL * */ @Component public class UserLoginFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) { ResponseUtils.responseJson(response, ResponseUtils.response(500, "登录失败", exception.getMessage())); } }
  • 编写登出成功处理类
Copy
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.logout.LogoutSuccessHandler; import org.springframework.stereotype.Component; import com.c3stones.utils.ResponseUtils; /** * 登出成功处理类 * * @author CL * */ @Component public class UserLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { SecurityContextHolder.clearContext(); ResponseUtils.responseJson(response, ResponseUtils.response(200, "登出成功", null)); } }
  • 用户Service、Dao中添加认证和授权时需要的方法
Copy
import java.util.List; import com.baomidou.mybatisplus.extension.service.IService; import com.c3stones.entity.SysAuth; import com.c3stones.entity.SysRole; import com.c3stones.entity.SysUser; /** * 系统用户Service * * @author CL * */ public interface SysUserService extends IService<SysUser> { /** * 根据用户名称查询用户信息 * * @param username 用户名称 * @return */ SysUser findUserByUserName(String username); /** * 根据用户ID查询角色 * * @param userId 用户ID * @return */ List<SysRole> findRoleByUserId(Long userId); /** * 根据用户ID查询权限 * * @param userId 用户ID * @return */ List<SysAuth> findAuthByUserId(Long userId); }
Copy
import java.util.List; import org.springframework.stereotype.Service; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.c3stones.dao.SysUserDao; import com.c3stones.entity.SysAuth; import com.c3stones.entity.SysRole; import com.c3stones.entity.SysUser; import com.c3stones.service.SysUserService; /** * 系统用户Service实现 * * @author CL * */ @Service public class SysUserSerivceImpl extends ServiceImpl<SysUserDao, SysUser> implements SysUserService { /** * 根据用户名称查询用户信息 * * @param username 用户名称 * @return */ @Override public SysUser findUserByUserName(String username) { return this.baseMapper.selectOne( new QueryWrapper<SysUser>().lambda().eq(SysUser::getUsername, username).ne(SysUser::getStatus, "1")); } /** * 根据用户ID查询角色 * * @param userId 用户ID * @return */ @Override public List<SysRole> findRoleByUserId(Long userId) { return this.baseMapper.findRoleByUserId(userId); } /** * 根据用户ID查询权限 * * @param userId 用户ID * @return */ @Override public List<SysAuth> findAuthByUserId(Long userId) { return this.baseMapper.findAuthByUserId(userId); } }
Copy
import java.util.List; import org.apache.ibatis.annotations.Mapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.c3stones.entity.SysAuth; import com.c3stones.entity.SysRole; import com.c3stones.entity.SysUser; /** * 系统用户Dao * * @author CL * */ @Mapper public interface SysUserDao extends BaseMapper<SysUser> { /** * 根据用户ID查询角色 * * @param userId 用户ID * @return */ List<SysRole> findRoleByUserId(Long userId); /** * 根据用户ID查询权限 * * @param userId 用户ID * @return */ List<SysAuth> findAuthByUserId(Long userId); }
Copy
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.c3stones.dao.SysUserDao"> <!-- 根据用户ID查询角色 --> <select id="findRoleByUserId" resultType="com.c3stones.entity.SysRole" parameterType="long"> SELECT r.* FROM sys_role r LEFT JOIN sys_user_role ur ON ur.role_id = r.id WHERE ur.user_id = #{userId} </select> <!-- 根据用户ID查询权限 --> <select id="findAuthByUserId" resultType="com.c3stones.entity.SysAuth" parameterType="long"> SELECT a.* FROM sys_auth a LEFT JOIN sys_role_auth ra ON ra.auth_id = a.id LEFT JOIN sys_user_role ur ON ur.role_id = ra.role_id WHERE ur.user_id = #{userId} </select> </mapper>
  • 编写用户认证Service,必须实现org.springframework.security.core.userdetails.UserDetailsService
Copy
import java.util.HashSet; import java.util.List; import java.util.Set; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import com.c3stones.entity.SysRole; import com.c3stones.entity.SysUser; import com.c3stones.security.entity.SysUserDetails; import com.c3stones.service.SysUserService; /** * 用户登录Service * * @author CL * */ @Service public class SysUserDetailsService implements UserDetailsService { @Autowired private SysUserService sysUserService; /** * 根据用户名查用户信息 * * @param username 用户名 * @return 用户详细信息 */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { SysUser sysUser = sysUserService.findUserByUserName(username); if (sysUser != null) { SysUserDetails sysUserDetails = new SysUserDetails(); BeanUtils.copyProperties(sysUser, sysUserDetails); Set<GrantedAuthority> authorities = new HashSet<>(); // 角色集合 List<SysRole> roleList = sysUserService.findRoleByUserId(sysUserDetails.getId()); roleList.forEach(role -> { authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getRoleName())); }); sysUserDetails.setAuthorities(authorities); return sysUserDetails; } return null; } }
  • 编写用户登录验证处理类
Copy
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.LockedException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Component; import com.c3stones.security.entity.SysUserDetails; import com.c3stones.security.service.SysUserDetailsService; /** * 用户登录验证处理类 * * @author CL * */ @Component public class UserAuthenticationProvider implements AuthenticationProvider { @Autowired private SysUserDetailsService userDetailsService; /** * 身份验证 */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = (String) authentication.getPrincipal(); // 获取用户名 String password = (String) authentication.getCredentials(); // 获取密码 SysUserDetails sysUserDetails = (SysUserDetails) userDetailsService.loadUserByUsername(username); if (sysUserDetails == null) { throw new UsernameNotFoundException("用户名不存在"); } if (!new BCryptPasswordEncoder().matches(password, sysUserDetails.getPassword())) { throw new BadCredentialsException("用户名或密码错误"); } if (sysUserDetails.getStatus().equals("2")) { throw new LockedException("用户已禁用"); } return new UsernamePasswordAuthenticationToken(sysUserDetails, password, sysUserDetails.getAuthorities()); } /** * 支持指定的身份验证 */ @Override public boolean supports(Class<?> authentication) { return true; } }
  • 编写用户权限注解处理类
Copy
import java.io.Serializable; import java.util.HashSet; import java.util.List; import java.util.Set; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.PermissionEvaluator; import org.springframework.security.core.Authentication; import org.springframework.stereotype.Component; import com.c3stones.entity.SysAuth; import com.c3stones.security.entity.SysUserDetails; import com.c3stones.service.SysUserService; /** * 用户权限注解处理类 * * @author CL * */ @Component public class UserPermissionEvaluator implements PermissionEvaluator { @Autowired private SysUserService sysUserService; /** * 判断是否拥有权限 * * @param authentication 用户身份 * @param targetUrl 目标路径 * @param permission 路径权限 * * @return 是否拥有权限 */ @Override public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) { SysUserDetails sysUserDetails = (SysUserDetails) authentication.getPrincipal(); Set<String> permissions = new HashSet<String>(); // 用户权限 List<SysAuth> authList = sysUserService.findAuthByUserId(sysUserDetails.getId()); authList.forEach(auth -> { permissions.add(auth.getPermission()); }); // 判断是否拥有权限 if (permissions.contains(permission.toString())) { return true; } return false; } @Override public boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission) { return false; } }

6. 编写JWT过滤器#

Copy
import java.io.IOException; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; import com.c3stones.security.config.JWTConfig; import com.c3stones.security.entity.SysUserDetails; import com.c3stones.security.utils.JWTTokenUtil; /** * JWT权限过滤器,用于验证Token是否合法 * * @author CL * */ public class JWTAuthenticationFilter extends BasicAuthenticationFilter { public JWTAuthenticationFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws IOException, ServletException { // 取出Token String token = request.getHeader(JWTConfig.tokenHeader); if (token != null && token.startsWith(JWTConfig.tokenPrefix)) { SysUserDetails sysUserDetails = JWTTokenUtil.parseAccessToken(token); if (sysUserDetails != null) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( sysUserDetails, sysUserDetails.getId(), sysUserDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } }

7. 编写Spring Security核心配置类#

Copy
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler; import com.c3stones.security.UserAuthenticationProvider; import com.c3stones.security.UserPermissionEvaluator; import com.c3stones.security.filter.JWTAuthenticationFilter; import com.c3stones.security.handler.UserAccessDeniedHandler; import com.c3stones.security.handler.UserLoginFailureHandler; import com.c3stones.security.handler.UserLoginSuccessHandler; import com.c3stones.security.handler.UserLogoutSuccessHandler; import com.c3stones.security.handler.UserNotLoginHandler; /** * 系统安全核心配置 * * @author CL * */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) // 开启方法权限注解 public class SysSecurityConfig extends WebSecurityConfigurerAdapter { /** * 无权限处理类 */ @Autowired private UserAccessDeniedHandler userAccessDeniedHandler; /** * 用户未登录处理类 */ @Autowired private UserNotLoginHandler userNotLoginHandler; /** * 用户登录成功处理类 */ @Autowired private UserLoginSuccessHandler userLoginSuccessHandler; /** * 用户登录失败处理类 */ @Autowired private UserLoginFailureHandler userLoginFailureHandler; /** * 用户登出成功处理类 */ @Autowired private UserLogoutSuccessHandler userLogoutSuccessHandler; /** * 用户登录验证 */ @Autowired private UserAuthenticationProvider userAuthenticationProvider; /** * 用户权限注解 */ @Autowired private UserPermissionEvaluator userPermissionEvaluator; /** * 加密方式 * * @return */ @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); } /** * 注入自定义PermissionEvaluator * * @return */ @Bean public DefaultWebSecurityExpressionHandler userSecurityExpressionHandler() { DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler(); handler.setPermissionEvaluator(userPermissionEvaluator); return handler; } /** * 用户登录验证 */ @Override protected void configure(AuthenticationManagerBuilder auth) { auth.authenticationProvider(userAuthenticationProvider); } /** * 安全权限配置 */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() // 权限配置 .antMatchers(JWTConfig.antMatchers.split(",")).permitAll()// 获取白名单(不进行权限验证) .anyRequest().authenticated() // 其他的需要登陆后才能访问 .and().httpBasic().authenticationEntryPoint(userNotLoginHandler) // 配置未登录处理类 .and().formLogin().loginProcessingUrl("/login/submit")// 配置登录URL .successHandler(userLoginSuccessHandler) // 配置登录成功处理类 .failureHandler(userLoginFailureHandler) // 配置登录失败处理类 .and().logout().logoutUrl("/logout/submit")// 配置登出地址 .logoutSuccessHandler(userLogoutSuccessHandler) // 配置用户登出处理类 .and().exceptionHandling().accessDeniedHandler(userAccessDeniedHandler)// 配置没有权限处理类 .and().cors()// 开启跨域 .and().csrf().disable(); // 禁用跨站请求伪造防护 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 禁用session(使用Token认证) http.headers().cacheControl(); // 禁用缓存 http.addFilter(new JWTAuthenticationFilter(authenticationManager())); //// 添加JWT过滤器 } }

8. 编写测试Controller#

  • 编写注册用户Controller
Copy
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.c3stones.entity.SysUser; import com.c3stones.entity.SysUserRole; import com.c3stones.service.SysUserRoleService; import com.c3stones.service.SysUserService; import com.c3stones.utils.ResponseUtils; /** * 注册用户实例Contrller * * @author CL * */ @RestController @RequestMapping(value = "/register") public class RegisterController { @Autowired private BCryptPasswordEncoder bCryptPasswordEncoder; @Autowired private SysUserService sysUserService; @Autowired private SysUserRoleService sysUserRoleService; /** * 注册普通用户 * * @param username 用户名 * @param password 密码 * @return */ @RequestMapping(value = "/user") public ResponseUtils user(String username, String password) { SysUser sysUser = new SysUser(); sysUser.setUsername(username); sysUser.setPassword(bCryptPasswordEncoder.encode(password)); sysUser.setStatus("0"); sysUserService.save(sysUser); // 注册普通用户 SysUserRole sysUserRole = new SysUserRole(); sysUserRole.setUserId(sysUser.getId()); sysUserRole.setRoleId(2L); sysUserRoleService.save(sysUserRole); return ResponseUtils.success(sysUser); } }
  • 编写普通用户Controller
Copy
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.c3stones.entity.SysUser; import com.c3stones.security.entity.SysUserDetails; import com.c3stones.service.SysUserService; import com.c3stones.utils.ResponseUtils; /** * 普通用户Contrller * * @author CL * */ @RestController @RequestMapping(value = "/user") public class UserController { @Autowired private SysUserService sysUserSerivce; /** * 查询用户信息 * * @return */ @PreAuthorize(value = "hasPermission('/user/info', 'sys:user:info')") @RequestMapping(value = "/info") public ResponseUtils info() { SysUserDetails sysUserDetails = (SysUserDetails) SecurityContextHolder.getContext().getAuthentication() .getPrincipal(); SysUser sysUser = sysUserSerivce.getById(sysUserDetails.getId()); return ResponseUtils.success(sysUser); } }
  • 编写管理员Controller
Copy
import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import com.c3stones.entity.SysAuth; import com.c3stones.entity.SysRole; import com.c3stones.entity.SysUser; import com.c3stones.service.SysUserService; import com.c3stones.utils.ResponseUtils; /** * 管理员Contrller * * @author CL * */ @RestController @RequestMapping(value = "/admin") public class AdminController { @Autowired private SysUserService sysUserSerivce; /** * 查询用户信息 * * @return */ @PreAuthorize(value = "hasRole('ADMIN')") @RequestMapping(value = "/list") public ResponseUtils list() { List<SysUser> userList = sysUserSerivce.list(); return ResponseUtils.success(userList); } /** * 查询用户角色 * * @return */ @PreAuthorize(value = "hasRole('ADMIN') or hasPermission('/user/role', 'sys:role:info')") @RequestMapping(value = "/role") public ResponseUtils role(Long id) { List<SysRole> roleList = sysUserSerivce.findRoleByUserId(id); return ResponseUtils.success(roleList); } /** * 查询用户权限 * * @return */ @PreAuthorize(value = "hasAnyRole('ADMIN', 'USER') and hasPermission('/user/auth', 'sys:auth:info')") @RequestMapping(value = "/auth") public ResponseUtils auth(Long id) { List<SysAuth> authList = sysUserSerivce.findAuthByUserId(id); return ResponseUtils.success(authList); } }

9. 测试#

  使用curl插件进行测试,建议使用Postman测试。

Copy
# 注册用户: curl "http://127.0.0.1:8080/register/user?username=C3Stones&password=123456" # 结果: {"code":200,"msg":"成功","data":{"id":3,"username":"C3Stones","password":"$2a$10$Z6a7DSehk58ypqyWzfFAbOR0gaqpwVzY9aNXKqf4UhDCSJxsbDqDK","status":"0"}} # 查询用户信息: curl "http://127.0.0.1:8080/user/info" # 结果: {"code":401,"data":"Full authentication is required to access this resource","msg":"未登录"} # 不存在用户登录: curl -X POST "http://127.0.0.1:8080/login/submit?username=guest&password=123456" # 结果: {"code":500,"data":"用户名不存在","msg":"登录失败"} # 密码错误用户登录: curl -X POST "http://127.0.0.1:8080/login/submit?username=user&password=123" # 结果: {"code":500,"data":"用户名或密码错误","msg":"登录失败"} # 将刚刚注册的人的状态在数据库中改成2(禁用状态),登录: curl -X POST "http://127.0.0.1:8080/login/submit?username=C3Stones&password=123456" # 结果: {"code":500,"data":"用户已禁用","msg":"登录失败"} # 普通用户登录: curl -X POST "http://127.0.0.1:8080/login/submit?username=user&password=123456" # 结果: {"code":200,"data":{"token":"Bearer eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiIyIiwic3ViIjoidXNlciIsImlhdCI6MTU5NDcwNzA0OSwiaXNzIjoiQzNTdG9uZXMiLCJleHAiOjE1OTQ3OTM0NDksImF1dGhvcml0aWVzIjoiW3tcImF1dGhvcml0eVwiOlwiUk9MRV9VU0VSXCJ9XSJ9.PSwPsO-ECc6EHz84-nM881pMcMfbjOpzr5N2gpXj9ku-Z5YrjEP-_c08anrBalV2F4-MSA-oy8qQNM71b_QPSA"},"msg":"登录成功"} # 查询用户信息: curl -H "Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiIyIiwic3ViIjoidXNlciIsImlhdCI6MTU5NDcwNzA0OSwiaXNzIjoiQzNTdG9uZXMiLCJleHAiOjE1OTQ3OTM0NDksImF1dGhvcml0aWVzIjoiW3tcImF1dGhvcml0eVwiOlwiUk9MRV9VU0VSXCJ9XSJ9.PSwPsO-ECc6EHz84-nM881pMcMfbjOpzr5N2gpXj9ku-Z5YrjEP-_c08anrBalV2F4-MSA-oy8qQNM71b_QPSA" "http://127.0.0.1:8080/user/info" # 结果: {"code":200,"msg":"成功","data":{"id":2,"username":"user","password":"$2a$10$szHoqQ64g66PymVJkip98.Fap21Csy8w.RD8v5Dhq08BMEZ9KaSmS","status":"0"}} # 查询用户列表: curl -H "Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiIyIiwic3ViIjoidXNlciIsImlhdCI6MTU5NDcwNzA0OSwiaXNzIjoiQzNTdG9uZXMiLCJleHAiOjE1OTQ3OTM0NDksImF1dGhvcml0aWVzIjoiW3tcImF1dGhvcml0eVwiOlwiUk9MRV9VU0VSXCJ9XSJ9.PSwPsO-ECc6EHz84-nM881pMcMfbjOpzr5N2gpXj9ku-Z5YrjEP-_c08anrBalV2F4-MSA-oy8qQNM71b_QPSA" "http://127.0.0.1:8080/admin/list" # 结果: {"code":403,"data":"不允许访问","msg":"拒绝访问"} # 管理员登录: curl -X POST "http://127.0.0.1:8080/login/submit?username=admin&password=123456" # 结果: {"code":200,"data":{"token":"Bearer eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiIxIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE1OTQ3MDc1MDAsImlzcyI6IkMzU3RvbmVzIiwiZXhwIjoxNTk0NzkzOTAwLCJhdXRob3JpdGllcyI6Ilt7XCJhdXRob3JpdHlcIjpcIlJPTEVfQURNSU5cIn1dIn0.3yL2Lpbmau5X6PA1OmnE4FFwOzrqwRaFcRa8OfRAgHY45VVJfGfm5kp8qfk96HvigaPQvzf8HmMC_Xx75Lwr8Q"},"msg":"登录成功"} # 查询用户列表: curl -H "Authorization:Bearer eyJhbGciOiJIUzUxMiJ9.eyJqdGkiOiIxIiwic3ViIjoiYWRtaW4iLCJpYXQiOjE1OTQ3MDc1MDAsImlzcyI6IkMzU3RvbmVzIiwiZXhwIjoxNTk0NzkzOTAwLCJhdXRob3JpdGllcyI6Ilt7XCJhdXRob3JpdHlcIjpcIlJPTEVfQURNSU5cIn1dIn0.3yL2Lpbmau5X6PA1OmnE4FFwOzrqwRaFcRa8OfRAgHY45VVJfGfm5kp8qfk96HvigaPQvzf8HmMC_Xx75Lwr8Q" "http://127.0.0.1:8080/admin/list" # 结果: {"code":200,"msg":"成功","data":[{"id":1,"username":"admin","password":"$2a$10$5T851lZ7bc2U87zjt/9S6OkwmLW62tLeGLB2aCmq3XRZHA7OI7Dqa","status":"0"},{"id":2,"username":" user","password":"$2a$10$szHoqQ64g66PymVJkip98.Fap21Csy8w.RD8v5Dhq08BMEZ9KaSmS","status":"0"},{"id":3,"username":"C3Stones","password":"$2a$10$O.1ynDeJtlaG9roOJoUZzukc6aGfqFo/YW5ErRERQD2eC5r5cV9dC","status":"0"}]}

10. 权限注解示例#

权限注解 说明
hasRole('ADMIN') 拥有ADMIN角色才可访问
hasPermission('/user/list', 'sys:role:info') 拥有sys:role:info权限才可访问/user/list接口
hasAnyRole('ADMIN', 'USER') 拥有ADMIN角色或者USER角色均可访问
hasRole('ADMIN') and hasRole('USER') 拥有ADMIN角色和USER角色才可访问
hasRole('ADMIN') or hasPermission('/user/list', 'sys:role:info') 拥有ADMIN角色或者拥有sys:role:info权限均可访问
  其他权限注解请自行查找。

11. 项目地址#

  spring-security-jwt-demo

posted @   C3Stones  阅读(7305)  评论(3编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
点击右上角即可分享
微信分享提示
CONTENTS