第一步
我们需要建立一个springboot工程,然后进行依赖的添加 我这里的springboot工程的版本是2.7.14 引入下方的依赖时,没有引发依赖冲突
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--引入web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>RELEASE</version>
<scope>compile</scope>
</dependency>
<!--引入springsecurity依赖 作用: 安全框架,进行用户的认证和权限的管理-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 引入Mysql依赖 我这里使用的是Mysql8.0的依赖 -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<version>8.0.33</version>
</dependency>
<!-- 引入Mybatis 作用: 和数据库进行交互 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!-- 引入lombok 作用: 减少代码量通过注解生成类的有参无参以及日志-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- 引入JWT 作用: 进行用户信息的加密-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.2.1</version>
</dependency>
<!-- 引入redis依赖 作用: 为了后面进行token验证 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.7.14</version>
</dependency>
第二步
进行application.yml 配置文件的编写
# mysql注册
# com.mysql.cj.jdbc.Driver 表示数据库的驱动 mysql8.0 驱动
# com.mysql.jdbc.Driver 这个是mysql5.xx 本版的驱动
# jdbc:mysql://127.0.0.1:3306 表示数据库的地址和端口号 我这里是本地连接。
# security_study 表示的是数据库的名称
# useUnicode=true&characterEncoding=UTF-8& 设置字符编码集使用的是Unicode编码,字符集是UTF-8
#serverTimezone=Asia/Shanghai 设置时区 数据库8.0以上的版本需要设置时区,不设置时区会报错
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://127.0.0.1:3306/security_study?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
# 配置Mybatis
mybatis:
# 配置映射文件
mapper-locations: classpath:mapper/*.xml
# 配置实体映射包 作用: 在配置xml文件映射时我们可以不用写包名,直接写表名即可 下方的包表示的是项目
#中的实体类的包
type-aliases-package: com.hxtz.springsecurity13demo.entity
configuration:
# 开启将下划线转成驼峰命名。 数据库中的表的字段一般以下划线命名,而java中的属性一般是以驼峰命名。这里将其进行了转换。
map-underscore-to-camel-case: true
# 开启控制台打印日志
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 配置签名密钥 这个密钥用于的是jwt签名密钥 内容是字符串可以自己随便写 我这里是MrCui 是我的署名
my:
secretKey: "MrCui"
第三步 进行核心代码的编写。
(1) 编写实体类要和数据库中的字段对应起来
这里用到了4个注解
@Data 帮我们生成了这个类的set() get() ,toString() 等方法
@NoArgsConstructor 生成类的无参构造
@AllArgsConstructor 生成类的有参构造
@Builder 将类改为建造者模式。
package com.hxtz.springsecurity13demo.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @Author: cyb
* @Date: 2023/8/3 10:35
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SysUser implements Serializable {
private Integer userId;
private String username;
private String password;
private String sex;
private String address;
private Integer enabled;
private Integer accountNoExpired;
private Integer credentialsNoExpired;
private Integer accountNoLocked;
}
# 用户表
CREATE TABLE `sys_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`username` varchar(255) DEFAULT NULL COMMENT '登陆名',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
`sex` varchar(255) DEFAULT NULL COMMENT '性别',
`address` varchar(255) DEFAULT NULL COMMENT '地址',
`enabled` int(11) DEFAULT '1' COMMENT '是否启动账户0禁用 1启用',
`account_no_expired` int(11) DEFAULT '1' COMMENT '账户是否没有过期0已过期 1 正常',
`credentials_no_expired` int(11) DEFAULT '1' COMMENT '密码是否没有过期0已过期 1 正常',
`account_no_locked` int(11) DEFAULT '1' COMMENT '账户是否没有锁定0已锁定 1 正常',
PRIMARY KEY (`user_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
# 用户角色表
CREATE TABLE `sys_role_user` (
`uid` int(11) NOT NULL COMMENT '用户编号',
`rid` int(11) NOT NULL COMMENT '角色编号',
PRIMARY KEY (`uid`,`rid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
# 角色菜单表
CREATE TABLE `sys_role_menu` (
`rid` int(11) NOT NULL COMMENT '角色表的编号',
`mid` int(11) NOT NULL COMMENT '菜单表的编号',
PRIMARY KEY (`mid`,`rid`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
# 角色表
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`rolename` varchar(255) DEFAULT NULL COMMENT '角色名称,英文名称',
`remark` varchar(255) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
# 菜单表
CREATE TABLE `sys_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '编号',
`pid` int(11) DEFAULT NULL COMMENT '父级编号',
`name` varchar(255) DEFAULT NULL COMMENT '名称',
`code` varchar(255) DEFAULT NULL COMMENT '权限编码',
`type` int(11) DEFAULT NULL COMMENT '0代表菜单1权限2 url',
`delete_flag` tinyint(4) DEFAULT '0' COMMENT '0代表未删除,1 代表已删除',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;
(2)编写mapper层 查询用户信息用来进行用户的认证。
java文件中的 @Param("userName") 要和 #{userName} 对应
/**
* @Author: cyb
* @Date: 2023/8/4 16:29
* 获取用户信息
*/
@Mapper
public interface SysUserMapper {
/**
* 查询用户信息
* @param userName 这里的userName要和mapper文件对应起来。
* @return
*/
SysUser queryUserByUserName(@Param("userName") String userName);
}
<?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.hxtz.springsecurity13demo.mapper.SysUserMapper">
<select id="queryUserByUserName" resultType="sysUser">
select user_id,username,`password`,sex,address,enabled,account_no_expired,credentials_no_expired,account_no_locked FROM
sys_user
WHERE username = #{userName};
</select>
</mapper>
(3)编写Service层代码
/**
* @Author: cyb
* @Date: 2023/8/4 16:42
*/
public interface SysUserService {
SysUser queryUserByUserName(String userName);
}
/**
* @Author: cyb
* @Date: 2023/8/4 16:43
*/
@Service
public class SysUserServiceImpl implements SysUserService {
@Resource
private SysUserMapper sysUserMapper;
@Override
public SysUser queryUserByUserName(String userName) {
SysUser sysUser = sysUserMapper.queryUserByUserName(userName);
return sysUser;
}
}
(4) 实现 UserDetails接口 该接口是提供用户信息的核心接口。该接口实现仅仅存储用户的信息。
为什么要实现这个接口?参考这篇文章 一起搞清楚 Spring Security 中的 UserDetails
package com.hxtz.springsecurity13demo.component;
import com.hxtz.springsecurity13demo.entity.SysUser;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.List;
/**
* @Author: cyb
* @Date: 2023/8/4 16:52
*/
@Service
public class MyUserDetails implements UserDetails {
private SysUser sysUser;
private List<SimpleGrantedAuthority> authorityList;
public MyUserDetails() {
}
public MyUserDetails(SysUser sysUser) {
this.sysUser = sysUser;
}
/**
* 获取权限信息
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorityList;
}
/**
* 获取密码
* @return
*/
@Override
public String getPassword() {
String password = this.sysUser.getPassword();
this.sysUser.setPassword(null);
return password;
}
/**
* 获取用户名
* @return
*/
@Override
public String getUsername() {
return this.sysUser.getUsername();
}
/**
* 判断账户是否过期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return this.sysUser.getAccountNoExpired().equals(1);
}
/**
* 判单账户是否加锁
* @return
*/
@Override
public boolean isAccountNonLocked() {
return this.sysUser.getAccountNoLocked().equals(1);
}
/**
* 判断账户凭证是否过期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return this.sysUser.getCredentialsNoExpired().equals(1);
}
/**
* 判断账户是否启用
* @return
*/
@Override
public boolean isEnabled() {
return this.sysUser.getEnabled().equals(1);
}
public void setAuthorityList(List<SimpleGrantedAuthority> authorityList) {
this.authorityList = authorityList;
}
public SysUser getSysUser(){
return sysUser;
}
}
(5) 注册Bean到Spring容器中 主要做用是为了配置springSecurity
这里面注册了3个Bean
首先注册 UserDeatisService 这个类主要是用来返回用户的信息
其次注册了 PasswordEncoder 这个类主要是用来进行密码的加密解密的在新版本的springsecurity框架中规定必须要注册密码编译器
最后注册了 SecurityFilterChain 安全适配器 这个类主要是用来拦截请求的。 查看用户是否有访问这个的权限
SecurityFilterChain是security新版的做法,以前是我们通过匿名内部类来进行 WebSecurityConfigurerAdapter 这个类的编写。
package com.hxtz.springsecurity13demo.config;
import com.hxtz.springsecurity13demo.JwtCheckFilter;
import com.hxtz.springsecurity13demo.component.MyAuthenticationSuccessHandler;
import com.hxtz.springsecurity13demo.component.MyLogoutSuccessHandler;
import com.hxtz.springsecurity13demo.component.MyUserDetails;
import com.hxtz.springsecurity13demo.entity.SysUser;
import com.hxtz.springsecurity13demo.service.impl.SysMenuServiceImpl;
import com.hxtz.springsecurity13demo.service.impl.SysUserServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
/**
* @Author: cyb
* @Date: 2023/8/4 16:22
* security配置类
*/
@EnableGlobalMethodSecurity(prePostEnabled = true) // 启用全局方法权限处理
@Slf4j
public class MySecurityConfig {
@Resource
private SysUserServiceImpl userService;
// @Resource
// private SysMenuServiceImpl sysMenuService; // 目前还没有用到后续会用到 这个service主要用来查看用户的权限信息的。
@Resource
private MyAuthenticationSuccessHandler successHandler;
@Resource
private MyLogoutSuccessHandler logoutHandler;
@Resource
private JwtCheckFilter jwtCheckFilter;
/**
* 注册UserDetailsService
* @return
*/
@Bean
public UserDetailsService userDetailsService(){
return username -> {
// 在这里怎么处理username?
SysUser sysUser = userService.queryUserByUserName(username);
if (null==sysUser){
throw new UsernameNotFoundException("用户不存在");
}
/* 权限信息这里先不用管。目前我们只关注认证就可以了 后续会将这里进行修改。
// 获取用户权限信息
List<String> authList = sysMenuService.getUserMenuByUserId(sysUser.getUserId());
List<SimpleGrantedAuthority> collect = authList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
myUserDetails.setAuthorityList(collect); // 将用户的权限信息放入到用户详细信息中
*/
// 这里是用户的信息
MyUserDetails myUserDetails = new MyUserDetails(sysUser);
return myUserDetails;
};
}
/**
* 注册密码编译器
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 安全适配器
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable(); // 先关闭跨域请求。
http.formLogin().successHandler(successHandler).permitAll(); // 请求登录接口放行。
http.logout().logoutSuccessHandler(logoutHandler);
http.authorizeRequests().anyRequest().authenticated(); // 任何的请求都需要认证。
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 禁用session域
http.addFilterBefore(jwtCheckFilter, UsernamePasswordAuthenticationFilter.class); // 将自定义过滤器放在 过滤器之前
return http.build();
}
}