SpringSecurity安全框架基础入门(前后端分离)
1. 环境配置
- SpringBoot 2.7.11
- Maven 3.6.1
2. 依赖包
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
3. 数据库配置
CREATE DATABASE /*!32312 IF NOT EXISTS*/`school_secrity` /*!40100 DEFAULT CHARACTER SET utf8mb4 */;
USE `school_secrity`;
/*Table structure for table `user` */
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`username` varchar(20) NOT NULL COMMENT '用户名',
`password` varchar(512) NOT NULL COMMENT '用户密码',
`roler` char(2) NOT NULL DEFAULT '0' COMMENT '0 普通用户',
`isDelete` char(1) DEFAULT '0' COMMENT '0 存在 1删除',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
4. SpringBoot配置文件
server:
port: 8080
servlet:
context-path: /
# mybatis plus配置
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 日志输出
global-config:
db-config:
logic-delete-field: isDelete # 逻辑删除 需要加一个注解 @TableLogic
logic-not-delete-value: 0 # 未删除逻辑值
logic-delete-value: 1 # 已经删除逻辑值
# spring 配置
spring:
datasource:
driver-class-name: com.mysql.jdbc.Driver # 数据库连接驱动
url: jdbc:mysql://localhost:3306/school_secrity # 数据库Url
username: root # 数据库账号
password: 123456 # 数据库密码
thymeleaf:
cache: false # 不使用缓存
redis:
host: 192.168.244.128
port: 6379
5. 认证流程
1. 使用之前的配置
若不理解 JWT是什么可以先去了解下 JWT是什么再来看本篇文章
在使用之前,首先要关闭 spring security 默认的登录页面
因为是前后端分离,我们就可以不使用session域进行存储用户的登录态,这里使用的 JWT认证登录态
因为是前后端分离,所以我们就可以在登录成功之后使用 JWT给用户返回一个登录的令牌,来认证登录信息
然后我们要指定我们自己的登录接口,和使用spring security自带的一个密码加密对象所以就有了如下的配置文件
@Configuration
public class SecrityConfig extends WebSecurityConfigurerAdapter {
/**
* 密码加密对象
*
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 前后端分离项目,配置不使用Session 和 关闭 csrf 和 指定登录接口
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() // 关闭scrf
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)// 不使用Session
.and()
.authorizeRequests()
.antMatchers("/user/login").anonymous() // 登录接口可以匿名访问
.anyRequest().authenticated(); // 其他接口必须已经登录
// 配置认证过滤器 在这里可以先忽略这个
// http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override // 身份验证管理器
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
2. 编写认证实体类
在spring security中认证有专门的实体类封装数据,所以我们需要将实体类编写,这里需要实现接口UserDetails
即有了如下实体类
/**
* 创建用户登录的对象
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
/**
* 登录的用户对象
* 将我们自己编写的表对象实体注入到里面
*/
private User user;
/**
* 权限列表,这里认证流程可以忽略
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
/**
* 获取密码
* @return
*/
@Override
public String getPassword() {
return user.getPassword();
}
/**
* 获取用户名
* @return
*/
@Override
public String getUsername() {
return user.getUsername();
}
/**
* 账户是未过期的
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 账户是未被锁定的
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 凭据是未过期的
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 账户已启用的
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
}
3. 使用MybatisPlus生成Service和Mapper和表实体类
这里忽略不讲,使用逆向工程生成,或使用插件生成,或自己编写都可以
4.重写默认登录接口
在SpriingSecurity中有一个默认的登录接口,使用的是他自己的用户名和密码,我们需要重写,编程查询数据库得到用户名和密码
所以我们的Service层或单独的一个登录接口中要实现接口UserDetailsService
这里使用的是MybatisPlus生成的Service,在生成的Service中又实现了接口UserDetailsService
可以根据自己的习惯进行更改
所以就有了如下实现类
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
implements UserService, UserDetailsService {
/**
* 重写登录接口
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
System.out.println("执行重写登录接口");
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUsername,username);
// 数据库查询用户对象
User user = baseMapper.selectOne(wrapper);
if (Objects.isNull(user)){
throw new RuntimeException("用户名或者密码错误");
}
return new LoginUser(user);
}
}
5. 编写自己的Controller和Service业务层
我们在SecrityConfig
配置文件中声明了自己的Controller接口请求URL/user/login
所以我们在定义的时候也要写这样的URL,所以有了
如下的Controller类
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private UserService userService;
@RequestMapping("/login")
public Result login(@RequestBody User user){
Result login = userService.login(user);
return login;
}
}
Service层中添加代码
在接口中需要自己添加此方法的接口
@Override
public Result login(User user) {
System.out.println("进入Login");
// 将前端传输过来的用户对象封装成Springsecurity可以使用的对象
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(user.getUsername(),user.getPassword());
// 认证管理器,在配置文件中注释掉了,可以放开,并且添加
Authentication authenticate = null;
try {
authenticate = authenticationManager.authenticate(authenticationToken);
}catch (Exception e){
String message = e.getMessage();
System.out.println("message = " + message);
}
System.out.println("authenticate = " + authenticate);
if (Objects.isNull(authenticate)){
throw new RuntimeException("用户名或者密码错误");
}
// 通过DeBug看 authenticate.getPrincipal(); 本质就是我们重写之后的LoginUser类对象,所以可以强转
System.out.println("认证通过");
LoginUser principal = (LoginUser) authenticate.getPrincipal();
Map<String, Object> map = new HashMap<>();
map.put("id",principal.getUser().getId());
// 生成用户的JWT token
String jwtToken = JWTUtils.getJwtToken(map);
map.clear();
map.put("token",jwtToken);
return new Result(200,"成功",map);
}
6.注意事项
其中我们要注意我们的密码加密对象passwordEncoder
在此框架中,我们只需要将此对象注入即可以实现在密码校验上的自动校验,但是在密码存储时,需要我们自己调用encoder方法进行加密后存储
7. 认证过滤器
我们的登录接口是被配置文件放行的,所以我们可以直接访问,直接进入登录接口进行登录,若我们想要使用JWT生成的令牌进行登录态验证,我们就需要配置一个认证的过滤器,这个过滤器是在每次调用service层前执行的,所以就可以对我们每次请求过来的令牌进行认证,用户是否登录
所以就有如下Java代码
/**
* JWT过滤器
*/
@Component
public class JwtFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
// 从请求头中获取token
String token = request.getHeader("token");
if (Strings.hasText(token)){
try {
Jws<Claims> claimsJws = JWTUtils.parseJwtToken(token);
Object id = claimsJws.getBody().get("id");
System.out.println("用户的id = " + id);
// 通过用户ID去redis缓存中拿用户的信息
// 若用户不存在,那么是未登录 存在则已经登录
// 封装用户信息
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken("admin","123456",null);
// 其中这里的admin 123456 是需要从redis中获取到的数据装进去的并不是固定的
// 放入上下文对象
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} catch (SignatureException e) {
e.printStackTrace();
}
}
// 放行
filterChain.doFilter(request,response);
}
}
OncePerRequestFilter 我们实现这个接口之后,这个过滤器是只被执行一次的,因为过滤器在特殊情况下,是可能执行多次的
6. 认证代码执行顺序
- 前端请求控制器,因有过滤器的缘故,会直接进入过滤器
- 进入过滤器,因我们是登录接口,是没有token信息的,那么我们在过滤器中判断没有取到token将直接放行请求去service层登录
- 接下来会正式进入controller中,执行我们的代码逻辑,从controller中执行到我们的service层
- 进入service层,会调用我们自己的login方法,然后方法中会将我们的前端传输过来的用户对象封装成spring security可以识别的用户对象
- 然后使用我们的认证管理器对象,进行认证管理,没有抛出异常证明认证没有错误,我们需要调用认真管理器对象中的方法获取到我们的认证对象的信息
- 接下来就是判断对象是否为null 然后对我们的对象进行简单的封装,使用 JWT生成令牌并且返回
- 在之后的每次请求中需要带入令牌进行请求
7. 用户授权
首选需要在登录的基础上进行授权,授权目的是判断用户是否有权限进行操作这个接口,或是否有权限进行这个操作
在上面的基础上我们在SecurityConfig配置文件中的类上加上注解,开启注解方式的授权
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启注解形式的授权功能
1. 简单的实现授权
在controller中新创建一个test的控制器,有如下代码
@RequestMapping("/test")
@PreAuthorize("hasAnyAuthority('user')") // 判断当前是否有user权限
public Object test(){
return "123123123";
}
我们去调用这个方法,并且带入我们的token会提示我们没有权限,那么我们就实现了一个简单的权限添加
接下来是如何给用户添加权限
2. 给用户添加权限
我们在编写用户登录的实体类中,我们有一个获取用户权限的方法getAuthorities
,所以我们要使用这个方法,将用户的权限封装到这里,即就可以更改LoginUser对象,加入一个属性
/**
* 用户的权限集合
*/
private Set<String> set;
//LoginUserDetails要被存储到redis 但redis要求使用的数据必须序列化 但SimpleGrantedAuthority类型不是序列化
//需要忽略此属性
@JSONField(serialize = false)
private List<SimpleGrantedAuthority> authorities; // 用于封装
// 构造方法需要写一个不带authorities 只有 set 和 user 两个对象的构造
public LoginUser(User user, Set<String> set) {
this.user = user;
this.set = set;
}
3. 封装权限获取方法
/**
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (list!=null){
return list;
}
list = new ArrayList<>();
for (String s : set) {
list.add(new SimpleGrantedAuthority(s));
}
return list;
}
4. 修改过滤器中代码
我们将对象中添加的权限信息,那么在每次请求非登录接口时都需要带上我们的权限,让我们的权限认证系统去认证是否有权限继续执行
那么将在过滤器中修改如下代码
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
log.info("进入过滤器");
// 从请求头中获取token
String token = request.getHeader("token");
if (Strings.hasText(token)){
try {
Jws<Claims> claimsJws = JWTUtils.parseJwtToken(token);
Object id = claimsJws.getBody().get("id");
System.out.println("用户的id = " + id);
// 通过用户ID去redis缓存中拿用户的信息
// 若用户不存在,那么是未登录 存在则已经登录
// 封装用户信息
// 获取用户权限信息,这里需要查询数据库获取
ArrayList<SimpleGrantedAuthority> list = new ArrayList<>();
list.add(new SimpleGrantedAuthority("test"));
list.add(new SimpleGrantedAuthority("user"));
UsernamePasswordAuthenticationToken authenticationToken
= new UsernamePasswordAuthenticationToken("admin","123456",list);
// 放入上下文对象
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
} catch (SignatureException e) {
e.printStackTrace();
}
}
// 放行
filterChain.doFilter(request,response);
}
8. 自定义异常处理
1. 认证失败处理器
/**
* 认证失败异常处理
*/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse
response, AuthenticationException authException) throws IOException,
ServletException {
Result result= new Result(401, "用户认证失败!", null);
response.getWriter().println(result);
}
}
2. 授权失败处理器
/**
* 授权失败异常处理
*/
@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Result result = new Result(400, "授权失败", null);
response.getWriter().println(result);
}
}
3. 更改配置类
在上面两个异常处理器处理完成后我们需要将异常处理类的对象注入到security的配置文件中,然后就配置完成了
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPointImpl;
//配置异常处理器 这段代码在configure方法中定义
http.exceptionHandling()
.authenticationEntryPoint(authenticationEntryPointImpl)//认证
.accessDeniedHandler(accessDeniedHandlerImpl);//授权
9. 配置跨域设置
配置类
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
//设置允许跨域的路径
registry.addMapping("/**")
.allowedOriginPatterns("*")//允许跨域的域名
.allowCredentials(true)//允许携带cookie
.allowedMethods("GET","DELETE","PUT","POST")//跨域的请求方式
.allowedHeaders("*")//允许的header属性
.maxAge(3600);//跨域时间
}
}
然后在security中开启跨域,只需要在configure方法中添加如下代码
//springsecrity开启允许跨域
http.cors();