一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
一般Web应用的需要进行认证和授权。
认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
授权:经过认证后判断当前用户是否有权限进行某个操作
而认证和授权也是SpringSecurity作为安全框架的核心功能。
我所学习的正是,认证和授权和流程。
另外默认SpringBoot、Redis会调用。
新建一个SpringBoot工程。这是使用的依赖。
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-test
test
org.springframework.boot
spring-boot-starter-data-redis
com.baomidou
mybatis-plus-boot-starter
3.5.3.1
mysql
mysql-connector-java
org.projectlombok
lombok
com.alibaba
fastjson
1.2.58
com.fasterxml.jackson.datatype
jackson-datatype-jdk8
io.jsonwebtoken
jjwt
0.9.1
org.apache.commons
commons-pool2
javax.xml.bind
jaxb-api
2.3.0
com.sun.xml.bind
jaxb-impl
2.3.0
com.sun.xml.bind
jaxb-core
2.3.0
javax.activation
activation
1.1.1
启动之后,访问localhost:8888, 出现一下页面代表SpringSecurity生效了。
此时我们发现控制台生成:
我们向登录表单输入,这段密码和用户名user,即可登录通过。
当然,我们实际情况,可能会加一个redis做缓存,登录之后,用用户id -> 用户信息,存储到redis中。
这样我们登录之后,解析token,就是从redis查询。
图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException 。
FilterSecurityInterceptor:负责权限校验的过滤器。
Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
1. ...,在现在前后端分离项目,我们肯定是要自定义登录接口,不能用SpringSecurity的登录页面;
2. UserDetailService 是在内存中查找比较用户输入的用户名和密码,那我们需要是需要查询数据库比对的。
user.sql
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint NOT NULL COMMENT '主键',
`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
`email` varchar(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` varchar(128) DEFAULT NULL COMMENT '头像',
`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
`create_by` bigint DEFAULT NULL COMMENT '创建人的用户id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用户表';
application.yaml 中数据库配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
UserMapper
@Mapper
public interface UserMapper extends BaseMapper {
}
User
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName(value = "user")
public class User {
/**
* 主键
*/
@TableId
private Long id;
/**
* 用户名
*/
private String userName;
/**
* 昵称
*/
private String nickName;
/**
* 密码
*/
private String password;
/**
* 账号状态(0正常 1停用)
*/
private String status;
/**
* 邮箱
*/
private String email;
/**
* 手机号
*/
private String phonenumber;
/**
* 用户性别(0男,1女,2未知)
*/
private String sex;
/**
* 头像
*/
private String avatar;
/**
* 用户类型(0管理员,1普通用户)
*/
private String userType;
/**
* 创建人的用户id
*/
private Long createBy;
/**
* 创建时间
*/
private Date createTime;
/**
* 更新人
*/
private Long updateBy;
/**
* 更新时间
*/
private Date updateTime;
/**
* 删除标志(0代表未删除,1代表已删除)
*/
private Integer delFlag;
}
UserDetailServiceImpl
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDetailsImpl implements UserDetails {
private User user;
//private List authList;
//
//@JSONField(serialize = false)
//private List authorities; // SimpleGrantedAuthority对象不支持序列化,无法存入redis
//
//
//public UserDetailsImpl(User user, List authList) { // 将对应的权限字符串列表传入
// this.user = user;
// this.authList = authList;
//}
//
@Override
public Collection extends GrantedAuthority> getAuthorities() {
初始化之后,我们后续其他拦截器,也会获取; 没必要多次初始化;
//if(authorities != null){
// return authorities;
//}else{
// authorities = new ArrayList<>();
//}
//
第一次登录,封装UserDetails对象,初始化权限列表
//for (String auth : authList) {
// SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(auth);
// authorities.add(simpleGrantedAuthority); // 对,默认是个空的
//}
//return authorities;
return null;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() { // 下面bool值,全部响应为true,UserDetail对象返回校验过程中,会因为没权限报错
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
RedisConfig : redis配置类
@Configuration
public class RedisConfig {
@Bean
@SuppressWarnings(value = { "unchecked", "rawtypes" })
public RedisTemplate
Result : 后端统一封装响应
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Result {
private T data;
private String mes;
private Integer code;
public Result(String mes, Integer _code) {
}
public static Result success(T data){
return new Result(data,"操作成功",200);
}
public static Result success(String _mes){
return new Result(_mes,200);
}
public static Result error(String _mes,Integer _code){
return new Result(_mes,_code);
}
}
JwtUtil 工具类
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "sangeng";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
public static void main(String[] args) throws Exception {
// String jwt = createJWT("2123");
Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyOTY2ZGE3NGYyZGM0ZDAxOGU1OWYwNjBkYmZkMjZhMSIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTYzOTk2MjU1MCwiZXhwIjoxNjM5OTY2MTUwfQ.NluqZnyJ0gHz-2wBIari2r3XpPp06UMn4JS2sWHILs0");
String subject = claims.getSubject();
System.out.println(subject);
// System.out.println(claims);
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
FastJsonRedisSerializer
public class FastJsonRedisSerializer implements RedisSerializer
{
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
private Class clazz;
static
{
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
}
public FastJsonRedisSerializer(Class clazz)
{
super();
this.clazz = clazz;
}
@Override
public byte[] serialize(T t) throws SerializationException
{
if (t == null)
{
return new byte[0];
}
return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
}
@Override
public T deserialize(byte[] bytes) throws SerializationException
{
if (bytes == null || bytes.length <= 0)
{
return null;
}
String str = new String(bytes, DEFAULT_CHARSET);
return JSON.parseObject(str, clazz);
}
protected JavaType getJavaType(Class> clazz)
{
return TypeFactory.defaultInstance().constructType(clazz);
}
}
我们自定义UserDatailService 实现UserDatailService接口,注入到spring容器中,这样就会在SpringSecurity的认证流程中调用我们自定义的实现类。
@Service
public class UserDetailServiceImpl extends ServiceImpl< UserMapper, User> implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 用用户名查询对应User
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
// 判断是否空
User user = this.getOne(queryWrapper);
if(user == null){
throw new RuntimeException("用户名或密码错误");
}
// 将查询到user封装到自定义UserDeatail中
return new UserDetailsImpl(user);
}
}
我们看下图,我们输入的用户名和密码,最终会传入UserDetailService 对象,加载loadUserByUsername 方法,返回UserDtail对象。返回过程中,会与UserDetail中的password和username进行比对,不同则报错。
注意:如果要测试,需要往用户表中写入用户数据,并且如果你想让用户的密码是明文存储,需要在密码前加{noop}。例如 : 数据库中,username:sg, password: {noop}1234
这样登陆的时候就可以用sg作为用户名,1234作为密码来登陆了。
在实际中项目中,我一般不会明文存储,而是采用PasswordEncoder加密的方式;
我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
UserController
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/login")
Result login(@RequestBody User user){
return userService.login(user);
}
}
public interface UserService extends IService {
Result login(User user);
}
@Service
public class UserServiceImpl extends ServiceImpl implements UserService {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private RedisTemplate redisTemplate;
@Override
public Result login(User user) {
// AuthenticationManager authenticationManager 进行认证
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
// 如果认证通过、给出对应提示
if(Objects.isNull(authenticate)){
throw new RuntimeException("登录失败");
}
// 认证通过使用userid生成jwt
UserDetailsImpl udi = (UserDetailsImpl) authenticate.getPrincipal();
String userId = udi.getUser().getId().toString();
String token = JwtUtil.createJWT(userId);
HashMap map = new HashMap<>();
map.put("token",token);
// 把完整用户信息存入redis
redisTemplate.opsForValue().set("login:" +userId,udi);
return Result.success(map,"登录成功");
}
}
SpringSecurityConfig
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
spring:
# mysql配置
datasource:
url: jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=UTC
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# redis配置
redis:
# 默认0库
database: 0
#连接超时时间
timeout: 10000ms
port: 6379
host: 192.168.213.136
lettuce:
pool:
# 设置最大连接数
max-active: 1024
# 最大阻塞时间
max-wait: 10000ms
# 最大空间连接,默认8
max-idle: 200
# 最小空间连接,默认5
min-idle: 5
server:
port: 8888
以下跨域设置,postman测试是完全没有问题的。但是,我们是为了前后端分离,会有问题。
当登录之后,携带token的请求头,被jwt过滤器捕获解析之后,获得userId,用userId将从redis拿出UserDetailImpl封装到 SecurityContextHolder.getContext()中,被SpringSecurity过滤链捕获到,证明没有问题,因此放行访问资源。
/**
* jwt过滤器
*
* @author: qhx20040819
* @date: 2023-09-09 21:27
**/
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 设置跨域
response.setHeader("Access-Control-Allow-Origin", request.getHeader("Origin")); // 修改携带cookie,PS
response.setHeader("Access-Control-Allow-Methods", "GET,POST,DELETE,PUT");
response.setHeader("Access-Control-Allow-Headers", "Authorization,content-type"); // PS
// 预检请求缓存时间(秒),即在这个时间内相同的预检请求不再发送,直接使用缓存结果。
response.setHeader("Access-Control-Max-Age", "3600");
//获取token
String authorization = request.getHeader("Authorization");
if(StringUtils.isEmpty(authorization)){
filterChain.doFilter(request,response);
return;
}
String token = authorization.substring(6);
//解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
UserDetailsImpl udi = (UserDetailsImpl) redisTemplate.opsForValue().get(redisKey);
if(Objects.isNull(udi)){
throw new RuntimeException("用户未登录");
}
//存入SecurityContextHolder
//获取权限信息封装到Authentication中
UsernamePasswordAuthenticationToken authenticationToken = // 现在权限字段是null
new UsernamePasswordAuthenticationToken(udi,null,udi.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//放行
filterChain.doFilter(request, response);
}
}
在SpringSecurity中,会使用默认FiterSecuritInterceptor来进行权限校验,而它又是从SpringSecurityContex中Authentication,来获取其中的权限信息。判断当前用户是否拥有访问当前资源权限。
因此,我们只需要将权限信息存储到Authentication中即可,设置资源的访问权限。
用注解开启相关配置。
@EnableGlobalMethodSecurity(prePostEnabled = true)
在相应的资源上开启访问权限管控。
@RequestMapping("/hello")
@PreAuthorize("hasAnyAuthority('test')") // 限制访问权限字段
public String hello(){
return "hello";
}
我们之前UserDetailServiceImpl中loadUserByUsername()中,返回的UserDetailImpl对象,我们之前创建时,并没传入权限字段,我们先自定义权限字段模拟用户权限列表。
@Service
public class UserDetailServiceImpl extends ServiceImpl< UserMapper, User> implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 用用户名查询对应User
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
// 判断是否空
User user = this.getOne(queryWrapper);
if(user == null){
throw new RuntimeException("用户名或密码错误");
}
List authlist = new ArrayList<>(); // 暂且封装这样权限字段
authlist.add("test");
authlist.add("test0");
return new UserDetailsImpl(user,authlist);
}
}
相应的UserDetailImpl中也需要修改,seucrity过滤器链是从getAuthorities()方法中来获取用户权限字段的。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDetailsImpl implements UserDetails {
private User user;
private List authList;
@JSONField(serialize = false)
private List authorities; // SimpleGrantedAuthority对象不支持序列化,无法存入redis
public UserDetailsImpl(User user, List authList) { // 将对应的权限字符串列表传入
this.user = user;
this.authList = authList;
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
// 初始化之后,我们后续其他拦截器,也会获取; 没必要多次初始化;
if(authorities != null){
return authorities;
}else{
authorities = new ArrayList<>();
}
// 第一次登录,封装UserDetails对象,初始化权限列表
for (String auth : authList) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(auth);
authorities.add(simpleGrantedAuthority); // 对,默认是个空的
}
return authorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUserName();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
这是拿着携带token去请求,能访问成功。
就是我们真实的权限应该封装在数据库中。
一个用户,可以对应多个角色;一个角色,可以对应多个用户;
一个角色,可以拥有多个权限字段; 一个权限字段,可以被多个角色所拥有;
USE `test`;
/*Table structure for table `menu` */
DROP TABLE IF EXISTS `menu`;
CREATE TABLE `menu` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`menu_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '菜单名',
`path` varchar(200) DEFAULT NULL COMMENT '路由地址',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
`create_by` bigint(20) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(20) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`del_flag` int(11) DEFAULT '0' COMMENT '是否删除(0未删除 1已删除)',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4 COMMENT='菜单表';
/*Table structure for table `role` */
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`name` varchar(128) DEFAULT NULL,
`role_key` varchar(100) DEFAULT NULL COMMENT '角色权限字符串',
`status` char(1) DEFAULT '0' COMMENT '角色状态(0正常 1停用)',
`del_flag` int(1) DEFAULT '0' COMMENT 'del_flag',
`create_by` bigint(200) DEFAULT NULL,
`create_time` datetime DEFAULT NULL,
`update_by` bigint(200) DEFAULT NULL,
`update_time` datetime DEFAULT NULL,
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
/*Table structure for table `role_menu` */
DROP TABLE IF EXISTS `role_menu`;
CREATE TABLE `role_menu` (
`role_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`menu_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '菜单id',
PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
/*Table structure for table `user` */
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
`user_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '用户名',
`nick_name` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '昵称',
`password` varchar(64) NOT NULL DEFAULT 'NULL' COMMENT '密码',
`status` char(1) DEFAULT '0' COMMENT '账号状态(0正常 1停用)',
`email` varchar(64) DEFAULT NULL COMMENT '邮箱',
`phonenumber` varchar(32) DEFAULT NULL COMMENT '手机号',
`sex` char(1) DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
`avatar` varchar(128) DEFAULT NULL COMMENT '头像',
`user_type` char(1) NOT NULL DEFAULT '1' COMMENT '用户类型(0管理员,1普通用户)',
`create_by` bigint(20) DEFAULT NULL COMMENT '创建人的用户id',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint(20) DEFAULT NULL COMMENT '更新人',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`del_flag` int(11) DEFAULT '0' COMMENT '删除标志(0代表未删除,1代表已删除)',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
/*Table structure for table `user_role` */
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`user_id` bigint(200) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`role_id` bigint(200) NOT NULL DEFAULT '0' COMMENT '角色id',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
SELECT
DISTINCT m.`perms`
FROM
user_role ur
LEFT JOIN `role` r ON ur.`role_id` = r.`id`
LEFT JOIN `role_menu` rm ON ur.`role_id` = rm.`role_id`
LEFT JOIN `menu` m ON m.`id` = rm.`menu_id`
WHERE
user_id = 2
AND r.`status` = 0
AND m.`status` = 0
@TableName(value="menu")
@Data
@AllArgsConstructor
@NoArgsConstructor
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Menu implements Serializable {
private static final long serialVersionUID = -54979041104113736L;
@TableId
private Long id;
/**
* 菜单名
*/
private String menuName;
/**
* 路由地址
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 菜单状态(0显示 1隐藏)
*/
private String visible;
/**
* 菜单状态(0正常 1停用)
*/
private String status;
/**
* 权限标识
*/
private String perms;
/**
* 菜单图标
*/
private String icon;
private Long createBy;
private Date createTime;
private Long updateBy;
private Date updateTime;
/**
* 是否删除(0未删除 1已删除)
*/
private Integer delFlag;
/**
* 备注
*/
private String remark;
}
@Mapper
public interface MenuMapper extends BaseMapper
修改UserDetailServiceImpl。
@Service
public class UserDetailServiceImpl extends ServiceImpl< UserMapper, User> implements UserDetailsService {
@Autowired
private MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 用用户名查询对应User
LambdaQueryWrapper queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(User::getUserName,username);
// 判断是否空
User user = this.getOne(queryWrapper);
if(user == null){
throw new RuntimeException("用户名或密码错误");
}
List authList = menuMapper.selectPermsByUserId(user.getId());
return new UserDetailsImpl(user,authlist);
}
}
我们发现,就算我们登录输入的用户名和错误的:
1.SpringSecurity没有抛出异常 2.也有没有相关响应提示信息;
我是并不希望这样,因此要添加自定义异常处理器:
/**
* 授权异常处理器
**/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Result result = Result.error("权限不足", 403);
String jsonResult = JSON.toJSONString(result);
response.setStatus(200);
response.setContentType("application/json");
response.getWriter().write(jsonResult);
}
}
/**
* 认证异常处理器
**/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Result result = Result.error("您认证错误/请检查你的用户名或密码是否正确", 401);
String jsonResult = JSON.toJSONString(result);
response.setStatus(200);
response.setContentType("application/json");
response.getWriter().write(jsonResult);
}
}
在SecurityConfig配置中引入
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtFilter jwtFilter;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于登录接口 允许匿名访问
.antMatchers("/user/login").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated();
// 添加自定义token过滤器到链中
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
// 添加自定义异常处理器
http.exceptionHandling().accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
上面除了我们SpringSecurity异常,那么还有一些其他异常,我们不希望每次捕获,并手动抛出,因此可以转化为自定义异常,用SpringBoot的全局异常处理器捕获并且抛出;
我们现在是用postman测试,但是未来项目是设计到前后端分离项目交互的,都伴随着跨域的问题。
后端能接受到前端原生request对象,response对象,都涉及到跨域的问题。
SpringSecurity解决跨域
SringMvc解决跨域
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
// 设置允许跨域的路径
registry.addMapping("/**")
// 设置允许跨域请求的域名
.allowedOriginPatterns("*")
// 是否允许cookie
.allowCredentials(true)
// 设置允许的请求方式
.allowedMethods("GET", "POST", "DELETE", "PUT")
// 设置允许的header属性
.allowedHeaders("*")
// 跨域允许时间
.maxAge(3600);
}
}
自定义jwt过滤器解决跨域