一、了解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: Alphamilk
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();
}
}
版权归原作者 Alphamilk 所有, 如有侵权,请联系我们删除。