# SpringSecurity2023
**Repository Path**: J_M_Hao/spring-security2023
## Basic Information
- **Project Name**: SpringSecurity2023
- **Description**: SpringBoot + Spring Security + JWT
- **Primary Language**: Unknown
- **License**: Not specified
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 0
- **Created**: 2023-12-11
- **Last Updated**: 2023-12-12
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# Spring Security
## Spring Security概述
Spring Security是Spring框架的一个模块,用于提供身份验证(Authentication)、授权(Authorization)和其他安全功能的支持。它是一个强大且可定制的框架,用于保护Spring应用程序中的资源。
主要包括有以下特点:
1. **身份验证(Authentication):**
- Spring Security提供了多种身份验证机制,包括基于用户名和密码的认证、基于令牌的认证、LDAP认证等。
- 支持自定义身份验证提供者,使开发者能够集成各种认证方式。
2. **授权(Authorization):**
- Spring Security支持基于角色和权限的访问控制,可以定义哪些用户具有哪些角色和权限。
- 提供注解和XML配置两种方式来声明安全性规则。
3. **Web安全性:**
- 针对Web应用程序,Spring Security提供了一套完整的安全性特性,包括保护URL、CSRF防护、Session管理等。
- 支持基于表达式的访问控制,可以在方法级别和URL级别定义安全规则。
4. **方法级安全性:**
- 允许在方法级别定义安全性规则,确保只有具有适当权限的用户能够执行受保护的方法。
5. **记住我(Remember Me):**
- 提供"记住我"功能,使用户在注销后能够通过记住的令牌重新登录。
6. **单点登录(Single Sign-On,SSO):**
- Spring Security可以与其他单点登录解决方案(如OAuth、CAS等)集成,实现跨应用程序的单一身份认证。
7. **集成:**
- 可以轻松集成到Spring应用程序中,与Spring框架的其他模块无缝合作。
8. **可扩展性:**
- 允许开发者根据应用程序的需求进行自定义,实现特定的安全性逻辑。
9. **事件驱动:**
- Spring Security基于事件驱动的模型,允许开发者在安全事件发生时进行监听和处理。
官网(https://spring.io/projects/spring-security)
## 使用
### 引入依赖
```xml
org.springframework.boot
spring-boot-starter-web
org.springframework.boot
spring-boot-starter-security
```
Security 通常都是通过自定义`UserDetailsService`, `AuthenticationProvider`, `AuthenticationManager`,`UsernamePasswordAuthenticationFilter`其中的一种来实现的。
最常见的是通过`UserDetailsService`结合`UsernamePasswordAuthenticationFilter`方式来实现
### 创建`UserDetails`实体
在`Spring Security`中`UserDetails`接口包含了有关用户的基本信息(用户名、密码、权限、是否锁定等)。我们可以结合此接口以及用户表来扩展我们的实体类。
```java
package com.kfm.domain;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.kfm.util.GrantedAuthorityDeserializer;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;
import java.util.Set;
@AllArgsConstructor
@NoArgsConstructor
@Data
@Entity
@Table(name = "sys_user")
public class SysUser implements Serializable, UserDetails {
@Id
@Column(name = "id", nullable = false, length = 32, precision = 0)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", length = 20, precision = 1)
private String username;
@Column(name = "password", nullable = false, length = 60, precision = 2)
private String password;
@Column(name = "create_time")
private Date createTime;
@Column(name = "update_time", precision = 4)
private Date updateTime;
@Column(name = "state", precision = 5)
private Integer state;
/**
* JPA 新增时自动设置创建时间
*/
@PrePersist
public void prePersist() {
this.createTime = new Date();
this.state = 1;
}
/**
* JPA 更新时自动设置更新时间
*/
@PreUpdate
public void preUpdate() {
this.updateTime = new Date();
}
/**
* authorities 是一个权限集合,可以用于存储用户的权限信息
* 此字段不是数据库字段,用于封装用户权限
* getAuthorities() 方法返回用户的所有权限
*
* @JsonDeserialize(using = GrantedAuthorityDeserializer.class)
* 由于 SimpleGrantedAuthority 没有无参构造函数,所以需要自定义反序列化器
*/
@Transient
@JsonDeserialize(using = GrantedAuthorityDeserializer.class)
private Set extends GrantedAuthority> authorities;
@Transient
private boolean accountNonExpired;
@Transient
private boolean accountNonLocked;
@Transient
private boolean credentialsNonExpired;
@Transient
private boolean enabled;
/**
* 用户的账号是否未过期
* @return true 表示账号未过期
*/
public boolean isAccountNonExpired() {
return this.state == 1;
}
/**
* 用户的账号是否未锁定
* @return true 账号未锁定
*/
public boolean isAccountNonLocked() {
return this.state == 1;
}
/**
* 用户的密码是否未过期
* @return true 密码未过期
*/
public boolean isCredentialsNonExpired() {
return this.state == 1;
}
/**
* 用户是否可用
* @return true 账号状态可用
*/
public boolean isEnabled() {
return this.state == 1;
}
}
```
> `UserDetails`接口中的抽象方法都是通过`@Data`注解重写的,所以此处未看到重写接口中的抽象方法
### 实现UserDetailsService接口
`UserDetailsService`接口负责加载用户信息,在`loadUserByUsername`方法中,你可以从数据库、外部服务等地方获取用户信息。我们可以在`SysUserService`(用户表的Service)接口中继承此接口。
```java
public interface SysUserService extends UserDetailsService {}
```
**实现类**
```java
package com.kfm.service.impl;
import com.kfm.dao.SysUserRepository;
import com.kfm.domain.SysUser;
import com.kfm.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;
import java.util.HashSet;
import java.util.Set;
@Service
public class SysUserServiceImpl implements SysUserService {
@Autowired
private SysUserRepository sysUserRepository;
/**
* 根据用户名获取用户信息
* @param username 用户名
* @return 用户信息
* @throws UsernameNotFoundException 用户名不存在异常
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserRepository.findByUsername(username);
if (ObjectUtils.isEmpty(sysUser)){
throw new UsernameNotFoundException("用户名不存在");
}
// TODO 此处应该将该用户权限查询出来, 我们后续再改造,现在先默认
Set authorities = new HashSet<>();
// 设置角色权限
authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
// 设置资源权限
authorities.add(new SimpleGrantedAuthority("sys:user:add"));
sysUser.setAuthorities(authorities);
return sysUser;
}
}
```
### 自定义登录流程
```java
package com.kfm.security;
import com.fasterxml.jackson.databind.json.JsonMapper;
import com.kfm.util.JwtUtil;
import com.kfm.util.Resp;
import com.kfm.util.SecurityUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自定义登录流程
* 该过滤器继承自 UsernamePasswordAuthenticationFilter
* UsernamePasswordAuthenticationFilter 是 Spring Security 提供的默认登录过滤器
*/
@Slf4j // 日志注解
public class SecurityLoginFilter extends UsernamePasswordAuthenticationFilter {
public SecurityLoginFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
// 从请求中获取用户名和密码
String username = request.getParameter("username");
String password = request.getParameter("password");
if (!StringUtils.hasText(username) || !StringUtils.hasText(password)){
throw new RuntimeException("用户名或密码不能为空");
}
// 调用封装的工具类进行登录
SecurityUtil.login(username, password, getAuthenticationManager());
return SecurityUtil.getAuthentication();
}
/**
* 重写 successfulAuthentication 方法
* 该方法会在认证成功后调用
* @param request
* @param response
* @param chain
* @param authResult the object returned from the attemptAuthentication
* method.
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
Object principal = authResult.getPrincipal();
log.info("principal: {}", principal);
JsonMapper jsonMapper = new JsonMapper();
// 生成 jwt
String jwt = JwtUtil.sign(jsonMapper.writeValueAsString(principal));
response.setHeader("Authorization", jwt);
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(jsonMapper.writeValueAsString(Resp.ok(jwt)));
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
log.error("登录失败, {}", e);
Resp result;
if (e instanceof AccountExpiredException) {
//账号过期
result = Resp.error("账号过期");
} else if (e instanceof BadCredentialsException) {
//密码错误
result = Resp.error("密码错误");
} else if (e instanceof CredentialsExpiredException) {
//密码过期
result = Resp.error("密码过期");
} else if (e instanceof DisabledException) {
//账号不可用
result = Resp.error("账号不可用");
} else if (e instanceof LockedException) {
//账号锁定
result = Resp.error("账号锁定");
} else if (e instanceof InternalAuthenticationServiceException) {
//用户不存在
result = Resp.error("用户不存在");
}else{
//其他错误
result = Resp.error("未知异常");
}
response.setContentType("application/json;charset=utf-8");
JsonMapper jsonMapper = new JsonMapper();
response.getWriter().write(jsonMapper.writeValueAsString(result));
}
}
```
> 此处如果不使用`UsernamePasswordAuthenticationFilter`可以提供一个登录接口(`controller`)。该接口需要在配置当中放行,未授权访问需要授权的请求时,会返回401或者403状态码,前端可以根据这个进行路由提示处理
>
> ```java
> @RestController
> public class LoginController {
>
>
> @Autowired
> AuthenticationManager authenticationManager;
>
> @PostMapping(value = "login")
> public ResultInfo login(@RequestBody Map params) {
> UserInfo userInfo = SecurityUtils.login(params.get("username"), params.get("password"), authenticationManager);
> return ResultInfo.success(userInfo);
> }
> }
> ```
>
> 此时,就可以不写这个Filter。也不用在下一节配置此Filter
### 配置
```java
package com.kfm.config;
import com.kfm.security.JwtAuthenticationFilter;
import com.kfm.security.MyAuthenticationEntryPoint;
import com.kfm.security.SecurityLoginFilter;
import com.kfm.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.authentication.configuration.EnableGlobalAuthentication;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@EnableWebSecurity // 开启Spring Security
@EnableGlobalAuthentication // 开启全局认证
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private SysUserService sysUserService;
@Autowired
private MyAuthenticationEntryPoint myAuthenticationEntryPoint;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
/**
* 配置密码加密器
* 这里我们使用官方推荐的加密方式 BCryptPasswordEncoder
* 如果要自定义密码加密器, 实现 PasswordEncoder 接口即可
* @return
*/
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
/**
* 配置安全拦截机制
* @param http the {@link HttpSecurity}
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
//用于配置直接放行的请求
.antMatchers("/login").permitAll()
//其余请求都需要验证
.anyRequest().authenticated()
//授权码模式需要 会弹出默认自带的登录框
.and().httpBasic().disable()
//禁用跨站伪造
.csrf().disable()
// 禁用 session
.sessionManagement().disable()
// 异常处理
.exceptionHandling().authenticationEntryPoint(myAuthenticationEntryPoint);
// 添加 JWT 认证过滤器
http.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
// 自定义登录认证流程
http.addFilterBefore(new SecurityLoginFilter(authenticationManager()), UsernamePasswordAuthenticationFilter.class);
}
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// 用户认证service - 查询数据库的逻辑
provider.setUserDetailsService(sysUserService);
// 设置密码加密算法
provider.setPasswordEncoder(passwordEncoder());
auth.authenticationProvider(provider);
}
}
```
### 认证异常处理
```java
/**
* 用于处理认证过程中出现的异常。
* 如:未登录访问受保护的资源时,会抛出异常,此时就会调用该类的 commence 方法
*/
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
Resp result = Resp.error("请先登录");
JsonMapper json = new JsonMapper();
response.setContentType("application/json;charset=utf-8");
response.getWriter().write(json.writeValueAsString(result));
}
}
```