目录
一、了解SpringSecurity
1.1 什么是Spring Security?
1.2 Spring Security功能
1.3 Spring Security原理
1.4 RABC (Role-Based Access Control)
二、SpringSecurity简单案例
2.1 引入SpringSecurity依赖
?2.2 创建一个简单的Controller
三、SpringSecurity配置
3.1 自定义用户与密码
3.2 允许匿名访问路径
3.3 数据库实现登陆校验
3.4 实现角色权限访问
3.5 对密码进行B加密
3.6 结合Jwt实现多重校验
一、了解SpringSecurity
1.1 什么是Spring Security?
Spring Security 是一个强大且高度可定制的身份验证和访问控制框架。它是 Spring 生态系统的一部分,为基于 Spring 的应用提供了全面的安全服务。Spring Security 的设计目标是为应用的安全需求提供一个完整的解决方案,同时保持高度的灵活性和可扩展性。
1.2 Spring Security功能
Spring Security 提供的功能包括但不限于:
- 认证(Authentication):验证用户身份,通常需要用户名和密码。
- 授权(Authorization):确定已认证的用户可以访问哪些资源或执行哪些操作。
- CSRF 保护:防止跨站请求伪造攻击。
- 会话管理:处理用户的会话,包括会话的创建、维护和销毁。
- 加密和编码:提供加密和散列算法的支持。
- OAuth2 和 OpenID Connect 支持:集成 OAuth2 和 OpenID Connect 协议,实现第三方认证。
- CORS 支持:处理跨域资源共享(Cross-Origin Resource Sharing)请求。
- 安全配置:允许通过 XML 或 Java 配置来定制安全策略。
1.3 Spring Security原理
Spring Security 的工作原理涉及几个关键组件:
- SecurityContext:存储认证信息,如当前登录用户和他们的权限。
-
AuthenticationManager:负责用户认证,通常使用
UserDetailsService
来加载用户信息。 - AccessDecisionManager:决定用户是否有权访问特定资源。
-
Filter Chain:一系列的过滤器处理请求和响应,例如
UsernamePasswordAuthenticationFilter
用于处理用户名和密码的提交。
1.4 RABC (Role-Based Access Control)
RABC,即基于角色的访问控制,是一种常见的访问控制机制,用于管理用户对资源的访问权限。在 RABC 中,权限不是直接授予用户,而是授予用户所属的角色。每个用户可以拥有一个或多个角色,而每个角色则有一组相应的权限。这种机制简化了权限管理,因为只需更改用户的角色就可以改变他们的权限集。
二、SpringSecurity简单案例
2.1 引入SpringSecurity依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
2.2 创建一个简单的Controller
@RestController
public class HelloController {
@GetMapping("Hello")
public String Hello(){
return "Hello SpringSecurity";
}
}
2.3 运行后访问localhost:8080/Hello,会自动跳转到localhost:8080/login
这里的用户密码都在启动时候的控制台上 (注意:每一次启动密码都不一样)
用户名:user
密码: b82aa5e5-0a3a-466b-90a8-e94098877823(控制台上的一串密码)
登陆后成功访问
三、SpringSecurity配置
后面的配置都是基于小案例的基础上实现,请先完成上述的小案例
3.1 自定义用户与密码
由于每一次生成密码都是不固定的,对调试并不友好,springSecurity可以通过在application.yml中进行自定义设置用户和密码
spring:
security:
user:
name: alphaMilk
password: 123456
输入用户名和密码即可正常登陆并访问资源
3.2 允许匿名访问路径
在Spring Security框架中,允许某些路径或资源在未经过身份验证的情况下被访问,通常称为“允许匿名访问”。这种配置对于公共页面、登录页面、注册页面、API文档等是非常必要的,因为这些页面或资源需要对所有用户开放,无论他们是否已经登录。
以下通过配置类实现,用户能够匿名访问login页面
// 使用@Configuration标记此类为Spring的配置类
@Configuration
// 启用WebSecurity的自动配置,以便Spring Security可以管理Web安全
@EnableWebSecurity
public class SecurityConfiguration {
// 定义一个名为securityFilterChain的bean,该bean将负责构建和应用安全过滤器链
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 配置HttpSecurity对象,定义安全规则
http
// 授权HTTP请求,定义哪些URL需要什么类型的访问控制
.authorizeHttpRequests((authz) -> authz
// 允许"/user/login" URL匿名访问
.requestMatchers("/user/login").anonymous()
// 所有其他请求都需要认证才能访问
.anyRequest().authenticated())
// 启用HTTP Basic认证,默认情况下提供简单的用户名/密码认证
.httpBasic(Customizer.withDefaults());
// 构建并返回SecurityFilterChain
return http.build();
}
}
创建一个LoginController类
@RestController
@RequestMapping("user")
public class LoginController {
@GetMapping("/login")
public String Login(){
return "这是登陆资源页面";
}
}
重启系统后直接访问,不需要登陆即可获取资源.
3.3 数据库实现登陆校验
通过自己数据库的用户和密码,实现登陆。将之前的自定义用户密码(application.yml中)都删除掉,并执行以下操作:
用户表单:
-- 创建一个包含用户信息和角色的简化表
CREATE TABLE IF NOT EXISTS `users` (
`id` INT AUTO_INCREMENT PRIMARY KEY, -- 用户ID,自增主键
`username` VARCHAR(255) NOT NULL UNIQUE, -- 用户名,唯一且不能为空
`password` VARCHAR(255) NOT NULL, -- 密码,存储加密后的密码
`role` ENUM('ROLE_USER', 'ROLE_ADMIN') NOT NULL, -- 角色,预定义为'ROLE_USER'或'ROLE_ADMIN'
`enabled` TINYINT(1) NOT NULL DEFAULT 1 -- 用户状态,1表示启用,0表示禁用
);
注意:这里的role需要按照ROLE_身份 的方式进行存储以便springsecurity进行权限访问控制
插入案例数据 :
-- 插入示例用户数据
INSERT INTO `users` (`username`, `password`, `role`, `enabled`)
VALUES
('user1', '123456', 'ROLE_USER', 1),
('admin1', '123456', 'ROLE_ADMIN', 1),
('disabledUser', '123456', 'ROLE_USER', 0);
引入依赖:
<!-- 数据库依赖-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
配置数据源:
spring:
datasource:
url: jdbc:mysql://localhost:3306/ap_security?characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
创建User实体与mapper并加上启动注释
User实体
@TableName(value ="users")
@Data
public class Users implements Serializable {
/**
*
*/
@TableId(type = IdType.AUTO)
private Integer id;
/**
*
*/
private String username;
/**
*
*/
private String password;
/**
*
*/
private Object role;
/**
*
*/
private Integer enabled;
@TableField(exist = false)
private static final long serialVersionUID = 1L;
@Override
public boolean equals(Object that) {
if (this == that) {
return true;
}
if (that == null) {
return false;
}
if (getClass() != that.getClass()) {
return false;
}
Users other = (Users) that;
return (this.getId() == null ? other.getId() == null : this.getId().equals(other.getId()))
&& (this.getUsername() == null ? other.getUsername() == null : this.getUsername().equals(other.getUsername()))
&& (this.getPassword() == null ? other.getPassword() == null : this.getPassword().equals(other.getPassword()))
&& (this.getRole() == null ? other.getRole() == null : this.getRole().equals(other.getRole()))
&& (this.getEnabled() == null ? other.getEnabled() == null : this.getEnabled().equals(other.getEnabled()));
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((getId() == null) ? 0 : getId().hashCode());
result = prime * result + ((getUsername() == null) ? 0 : getUsername().hashCode());
result = prime * result + ((getPassword() == null) ? 0 : getPassword().hashCode());
result = prime * result + ((getRole() == null) ? 0 : getRole().hashCode());
result = prime * result + ((getEnabled() == null) ? 0 : getEnabled().hashCode());
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(getClass().getSimpleName());
sb.append(" [");
sb.append("Hash = ").append(hashCode());
sb.append(", id=").append(id);
sb.append(", username=").append(username);
sb.append(", password=").append(password);
sb.append(", role=").append(role);
sb.append(", enabled=").append(enabled);
sb.append(", serialVersionUID=").append(serialVersionUID);
sb.append("]");
return sb.toString();
}
}
UserMapper
public interface UsersMapper extends BaseMapper<Users> {
}
@MapperScan
@SpringBootApplication
@MapperScan("com.example.mysecurity.mapper")
public class MySecurityApplication {
public static void main(String[] args) {
SpringApplication.run(MySecurityApplication.class, args);
}
}
测试mybatisPlus是否配置正确
@SpringBootTest
class MySecurityApplicationTests {
@Autowired
private UsersMapper usersMapper;
@Test
void contextLoads() {
List<Users> users = usersMapper.selectList(null);
System.out.println(users);
}
}
通过后,即可开始实现通过自己数据库进行登陆功能:
先创建返回的验证类
LoginUser 实现 UserDetails
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails {
private Users user;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@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;
}
}
登陆实现类
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UsersMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根据用户名查询用户信息
LambdaQueryWrapper<Users> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(Users::getUsername,username);
Users user = userMapper.selectOne(wrapper);
//如果查询不到数据就通过抛出异常来给出提示
if(Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
Collection<? extends GrantedAuthority> authorities = Collections.singletonList(new SimpleGrantedAuthority(user.getRole()));
//封装成UserDetails对象返回
return new LoginUser(user,authorities);
}
}
由于在Spring Boot 2.3及更高版本中,Spring Security默认不再提供任何内置的PasswordEncoder
。这意味着如果在配置中直接使用明文密码或没有正确配置PasswordEncoder
,你将看到这个异常。这里暂时先使用明文加密。后面将一步步完善加密功能.
在SecurityConfig中加入配置
@Configuration
// 启用WebSecurity的自动配置,以便Spring Security可以管理Web安全
@EnableWebSecurity
public class SecurityConfiguration {
// 设置密码加密为明文加密
@Bean
public org.springframework.security.crypto.password.PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
// 定义一个名为securityFilterChain的bean,该bean将负责构建和应用安全过滤器链
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 配置HttpSecurity对象,定义安全规则
http
// 授权HTTP请求,定义哪些URL需要什么类型的访问控制
.authorizeHttpRequests((authz) -> authz
// 允许"/user/login" URL匿名访问
.requestMatchers("/user/login").anonymous()
// 所有其他请求都需要认证才能访问
.anyRequest().authenticated())
// 启用HTTP Basic认证,默认情况下提供简单的用户名/密码认证
.httpBasic(Customizer.withDefaults());
// 构建并返回SecurityFilterChain
return http.build();
}
}
再次访问localhost:8080/Hello后弹出登陆框:
输入任意的用户与密码即可正常访问
3.4 实现角色权限访问
在Controller中定义一个Admin资源类,只有admin用户才能进行访问
@RequestMapping("admin")
@RestController
@Slf4j
public class AdminController {
@GetMapping("resourse")
public String AdminRole(){
return "这是只有管理员用户才能访问的资源";
}
}
在设置中进行配置
@Configuration
// 启用WebSecurity的自动配置,以便Spring Security可以管理Web安全
@EnableWebSecurity
public class SecurityConfiguration {
// 设置密码加密为明文加密
@Bean
public org.springframework.security.crypto.password.PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
// 定义一个名为securityFilterChain的bean,该bean将负责构建和应用安全过滤器链
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 配置HttpSecurity对象,定义安全规则
http
// 授权HTTP请求,定义哪些URL需要什么类型的访问控制
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/user/login").anonymous()
// 需要有Admin身份的用户才能进行访问
.requestMatchers("/admin/**").hasRole("ADMIN")
// 所有其他请求都需要认证才能访问
.anyRequest().authenticated())
// 启用HTTP Basic认证,默认情况下提供简单的用户名/密码认证
.httpBasic(Customizer.withDefaults());
// 构建并返回SecurityFilterChain
return http.build();
}
}
重启服务器后分别用两种身份进行访问
用户访问:
管理员访问:
3.5 对密码进行B加密
config中进行配置BCrypt
@Configuration
// 启用WebSecurity的自动配置,以便Spring Security可以管理Web安全
@EnableWebSecurity
public class SecurityConfiguration {
@Autowired
private UserDetailsServiceImpl userDetailsService;
// 设置密码加密为B加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
// 定义一个名为securityFilterChain的bean,该bean将负责构建和应用安全过滤器链
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 配置HttpSecurity对象,定义安全规则
http
// 授权HTTP请求,定义哪些URL需要什么类型的访问控制
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/user/login").anonymous()
// 需要有Admin身份的用户才能进行访问
.requestMatchers("/admin/**").hasRole("ADMIN")
// 所有其他请求都需要认证才能访问
.anyRequest().authenticated())
// 启用HTTP Basic认证,默认情况下提供简单的用户名/密码认证
.httpBasic(Customizer.withDefaults());
// 构建并返回SecurityFilterChain
return http.build();
}
}
由于数据库中都是明文的密码,所以这里可以通过创建一个SpringbootTest类,将所有用户的密码改为B加密后的数据.
@Test
public void testUpdateAllPasswords() {
// 创建一个BCryptPasswordEncoder实例
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
// 更新所有用户的密码为加密后的"123456"
String encodedPassword = encoder.encode("123456");
// 构造更新条件
LambdaUpdateWrapper<Users> updateWrapper = new LambdaUpdateWrapper<>();
updateWrapper.set(Users::getPassword, encodedPassword);
// 执行更新操作
boolean result = usersMapper.update(null, updateWrapper) > 0;
if (result) {
System.out.println("所有用户的密码更新成功!");
} else {
System.out.println("密码更新失败!");
}
}
配置好后,重新进行登陆查看输入对应的admin1和123456
3.6 结合Jwt实现多重校验
Spring Security 和 JSON Web Tokens (JWT) 可以协同工作来提供更灵活和安全的身份验证和授权机制。尽管 Spring Security 提供了一套全面的安全框架,但它默认使用基于会话的认证机制,这意味着服务器维护着与客户端的活动会话状态。而JWT提供了一种无状态的认证方式,这意味着每个请求都包含完整的认证信息,无需服务器保存会话状态。
pom文件中引入jwt所需要的依赖
<!-- JWT依赖-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Jwt资源类:
@Component
@ConfigurationProperties(prefix = "jwt")
@Data
public class JwtProperties {
private String SecretKey;
private long Ttl;
private String TokenName;
}
Jwt yml配置:
jwt:
secret-key:
token-name: Authorization
ttl: 10800000
实现Jwt的工具类
@Component
public class JwtUtil {
@Autowired
private JwtProperties jwtProperties;
public String createJWT(Map<String, Object> claims, long ttlMillis) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
long expMillis = System.currentTimeMillis() + ttlMillis;
Date exp = new Date(expMillis);
return Jwts.builder()
.setClaims(claims)
.signWith(signatureAlgorithm, jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8))
.setExpiration(exp)
.compact();
}
public Claims parseJWT(String token) {
return Jwts.parser()
.setSigningKey(jwtProperties.getSecretKey().getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(token)
.getBody();
}
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
private String getUsernameFromToken(String token) {
Claims claims = parseJWT(token);
return (String) claims.getSubject();
}
private boolean isTokenExpired(String token) {
try {
final Date expiration = parseJWT(token).getExpiration();
return expiration.before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
}
Jwt的校验Filter
@Component
public class JwtFilter extends OncePerRequestFilter {
@Autowired
private JwtProperties jwtProperties;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String token = request.getHeader(jwtProperties.getTokenName());
if (token != null) {
try {
Claims claims = jwtUtil.parseJWT(token);
String username = (String) claims.get("userName");
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(username);
if (jwtUtil.isTokenValid(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (ExpiredJwtException ex) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Token has expired.");
} catch (JwtException ex) {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid token.");
}
}else {
response.sendError(HttpServletResponse.SC_FORBIDDEN, "No token provided.");
}
filterChain.doFilter(request, response);
}
}
将jwt校验规则加入到Spring的Filter中进行校验
@Configuration
// 启用WebSecurity的自动配置,以便Spring Security可以管理Web安全
@EnableWebSecurity
public class SecurityConfiguration {
@Autowired
private JwtFilter jwtFilter;
@Autowired
private UserDetailsServiceImpl userDetailsService;
// 设置密码加密为B加密
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
// 定义一个名为securityFilterChain的bean,该bean将负责构建和应用安全过滤器链
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// 配置HttpSecurity对象,定义安全规则
http
// 授权HTTP请求,定义哪些URL需要什么类型的访问控制
.authorizeHttpRequests((authz) -> authz
.requestMatchers("/user/login").anonymous()
// 需要有Admin身份的用户才能进行访问
.requestMatchers("/admin/**").hasRole("ADMIN")
// 所有其他请求都需要认证才能访问
.anyRequest().authenticated())
// 启用HTTP Basic认证,默认情况下提供简单的用户名/密码认证
.httpBasic(Customizer.withDefaults());
// 加入jwtFIlter
http.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
// 构建并返回SecurityFilterChain
return http.build();
}
}