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
<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
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为例,其余请自行添加。
创建实体类:
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类:
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:
<?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:
import com.baomidou.mybatisplus.extension.service.IService;
import com.c3stones.entity.SysUser;
/**
* 系统用户Service
*
* @author CL
*
*/
public interface SysUserService extends IService<SysUser> {
}
创建Service实现类:
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:
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配置信息类:
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. 编写工具类
- 创建响应工具类
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工具类:
/**
* 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核心实现类
- 编写无权限处理类
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()));
}
}
- 编写未登录处理类
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()));
}
}
- 编写登录成功处理类
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));
}
}
- 编写登录失败处理类
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()));
}
}
- 编写登出成功处理类
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中添加认证和授权时需要的方法
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);
}
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);
}
}
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);
}
<?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
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;
}
}
- 编写用户登录验证处理类
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;
}
}
- 编写用户权限注解处理类
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过滤器
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核心配置类
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
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
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
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测试。
# 注册用户:
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. 项目地址
SpringBoot + SpringSecurity + Mybatis-Plus + JWT实现分布式系统认证和授权的更多相关文章
- SpringBoot + SpringSecurity + Mybatis-Plus + JWT + Redis 实现分布式系统认证和授权(刷新Token和Token黑名单)
1. 前提 本文在基于SpringBoot整合SpringSecurity实现JWT的前提中添加刷新Token以及添加Token黑名单.在浏览之前,请查看博客: SpringBoot + Sp ...
- springboot+springsecurity+mybatis plus注解实现对方法的权限处理
文章目录 接上文 [springboot+springsecurity+mybatis plus之用户授权](https://blog.csdn.net/Kevinnsm/article/detail ...
- SpringSecurity04 利用JPA和SpringSecurity实现前后端分离的认证和授权
1 环境搭建 1.1 环境说明 JDK:1.8 MAVEN:3.5 SpringBoot:2.0.4 SpringSecurity:5.0.7 IDEA:2017.02旗舰版 1.2 环境搭建 创建一 ...
- SpringBoot日记——Spring的安全配置-登录认证与授权
安全是每个项目开发中都需要考虑的,比如权限控制,安全认证,防止漏洞攻击等. 比较常见的安全框架有:Apache的shiro.Spring Security等等,相信用shiro的用户群体更多,而sec ...
- springboot+springsecurity+mybatis plus之用户认证
一.权限管理的概念 另一个安全框架shiro:shiro之权限管理的描述 导入常用坐标 <dependency> <groupId>org.springframework.bo ...
- springboot+springsecurity+mybatis plus之用户授权
文章目录 前言 一.导入坐标 二.Users实体类及其数据库表的创建 三.controller,service,mapper层的实现 四.核心--编写配置文件 五.无权限界面和登录界面的实现 前言 即 ...
- 基于SpringBoot+SpringSecurity+mybatis+layui实现的一款权限系统
这是一款适合初学者学习权限以及springBoot开发,mybatis综合操作的后台权限管理系统 其中设计到的数据查询有一对一,一对多,多对多,联合分步查询,充分利用mybatis的强大实现各种操作, ...
- SpringBoot + SpringSecurity + Quartz + Layui实现系统权限控制和定时任务
1. 简介 Spring Security是一个功能强大且易于扩展的安全框架,主要用于为Java程序提供用户认证(Authentication)和用户授权(Authorization)功能. ...
- Core篇——初探Core的认证,授权机制
目录 1.Cookie-based认证的实现 2.Jwt Token 的认证与授权 3.Identity Authentication + EF 的认证 Cookie-based认证的实现 cooki ...
随机推荐
- 死磕以太坊源码分析之Kademlia算法
死磕以太坊源码分析之Kademlia算法 KAD 算法概述 Kademlia是一种点对点分布式哈希表(DHT),它在容易出错的环境中也具有可证明的一致性和性能.使用一种基于异或指标的拓扑结构来路由查询 ...
- 卷积神经网络图像纹理合成 Texture Synthesis Using Convolutional Neural Networks
代码实现 概述 这是关于Texture Synthesis Using Convolutional Neural Networks论文的tensorflow2.0代码实现,使用keras预训练的VGG ...
- 在 macOS 中使用 Podman
原文链接:https://fuckcloudnative.io/posts/use-podman-in-macos/ Podman 是一个无守护程序与 Docker 命令兼容的下一代 Linux 容器 ...
- linux中的fork炸弹
学习bash脚本看到一段代码(老鸟应该知道)挺有意思,一时看不懂.该命令不需要管理员即可运行,请不要在你的机器上使用以下脚本,否则你知道你在干什么 :() { :|: & };: 参考链接:h ...
- FL Studio中如何使用插件混杂功能中的琶音器
琶音指一串和弦音从低到高或从高到低依次连续奏出,可视为分解和弦的一种.通常作为一种专门的技巧训练用于练习曲中,有时作为短小的连接句或经过句出现在乐曲旋律声部中.在Trance类型电子音乐中,琶音的运用 ...
- Android应用测试指南
一.Android 的 SDK Windows 版本安装 按顺序安装以下内容 1. 安装JDK(Java Development Kit, 即Java开发工具包) 2. 安装Eclipse 集成 ...
- 关于String类的一些知识点
//原链接:https://blog.csdn.net/songylwq/article/details/7297004 String str=new String("abc"); ...
- D - Number of Multisets 题解(思维dp)
题目链接 题目大意 给你一个数k和n,表示用n个\(1/2^i(i=0,1,2.....)\)组成k有多少种方案数 题目思路 这个dp实属巧妙 设\(dp[i][j]表示i个数构成j\) 这i个数可以 ...
- Java线程池一:线程基础
最近精读Netty源码,读到NioEventLoop部分的时候,发现对Java线程&线程池有些概念还有困惑, 所以深入总结一下 线程创建 Java线程创建主要有三种方式:继承Thread类.实 ...
- 1. 揭秘Spring类型转换 - 框架设计的基石
仰不愧天,俯不愧人,内不愧心.关注公众号[BAT的乌托邦],有Spring技术栈.MyBatis.JVM.中间件等小而美的原创专栏供以免费学习.分享.成长,拒绝浅尝辄止.本文已被 https://ww ...