一、知识准备
Spring Security 是Spring家族中基于JavaEE的企业Web应用程序的安全服务框架。准确而言是基于JavaEE中Servlet规范的Filter机制。
根据Servlet规范:一个客户端请求Request在Servlet容器中需要经过Filter Chain中一些列Filter处理后才会获取到Web资源,而且响应Response也需要再次经过Filter Chain中的Filter处理后才能返回给客户端。Spring基于该Servlet规范,在Filter中接入安全机制来保证Web资源的安全。
Spring Security主要就做这2件事:
1: 身份认证(谁在发起请求),
2:身份授权(是否有权限访问资源)。
但是需要明确一点:FilterSecurityInterceptor主要做的是基于访问规则的身份授权。而身份认证是身份授权的前提,因此FilterSecurityInterceptor会在认证信息不存在时进行一次身份认证。正常认证流程是在其他优先级更高的过滤器完成的身份认证,当然二者的认证流程一致:
- 通过AuthenticationManager获取当前请求的身份认证信息
- 通过AccessDecisionManager决断特定访问规则的web资源能否被访问
二、身份认证流程
在Spring Security中进行身份认证的流程如:
1、过滤器链(FilterChain)(内部组件,不需配置):SpringSecurity的过滤器链负责处理HTTP请求并进行身份认证。过滤器链是由一系列过滤器组成的链条,其中包括UsernamePasswordAuthenticationFilter等过滤器。过滤器链根据请求的URL和配置的规则来确定是否对请求进行身份认证处理。
2、UsernamePasswordAuthenticationFilter(内部组件,不需配置):这个过滤器负责处理基于用户名和密码的身份认证请求。它从请求中获取用户名和密码,并将其封装成UsernamePasswordAuthenticationToken对象,然后将该对象传递给下一个组件。
3、AuthenticationManager(需要配置):AuthenticationManager是SpringSecurity中的一个核心接口,负责管理身份认证。当UsernamePasswordAuthenticationFilter传递UsernamePasswordAuthenticationToken给AuthenticationManager时,它会委托给一个或多个AuthenticationProvider来完成具体的身份认证过程。
4、AuthenticationProvider(默认不需配置,也可以自定义):AuthenticationProvider是一个接口,它定义了对特定类型的身份认证进行处理的方法。常见的实现类是DaoAuthenticationProvider,它使用用户名和密码进行身份认证。AuthenticationProvider依赖于UserDetailsService来获取用户的详细信息,并使用PasswordEncoder来进行密码的比对。
5、UserDetailsService(需要配置):UserDetailsService是一个接口,用于加载用户的详细信息。它根据用户名从数据库或其他存储中加载用户信息,包括密码和权限等。你可以自定义实现该接口,或使用Spring Security提供的默认实现。
6、PasswordEncoder(默认不需配置,也可以自定义):PasswordEncoder是一个接口,用于对密码进行编码和比对。在身份认证过程中,AuthenticationProvider使用PasswordEncoder来比对用户提交的密码和数据库中存储的密码是否匹配。常见的实现类是BCryptPasswordEncoder,它使用bcrypt算法进行密码的加密和比对。
7、Authentication(内部组件,不需配置):当身份认证成功时,AuthenticationProvider会创建一个已认证的Authentication对象,其中包含了用户的身份信息和权限信息。该对象将被封装到SecurityContextHolder中的安全上下文中,供后续的权限控制和访问控制使用。
8、登录成功或失败处理器:根据认证结果,UsernamePasswordAuthenticationFilter会根据配置的登录成功和失败处理器,进行相应的处理。例如,可以进行重定向到指定的页面或返回认证失败的错误信息。
三、编码流程
Spring Boot项目中使用Spring Security的配置和实现:
1、添加Spring Security依赖:在项目的
pom.xml
文件中添加以下依赖项:
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
2、创建Spring Security配置类:
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/user/**").hasAnyRole("ADMIN", "USER")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3、实现UserDetailsService接口:
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("User not found");
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
getAuthorities(user.getRoles()));
}
private Collection<? extends GrantedAuthority> getAuthorities(Set<Role> roles) {
return roles.stream()
.map(role -> new SimpleGrantedAuthority(role.getName()))
.collect(Collectors.toList());
}
}
4、创建登录页面控制器
@Controller
public class LoginController {
@GetMapping("/login")
public String login() {
return "login";
}
}
四、Spring Security整合JWT
Spring Security 和 JWT(JSON Web Token)的整合可以用于构建安全的、基于令牌的身份验证和授权机制。
1、添加相关依赖:在
pom.xml
文件中添加以下依赖:
<!-- Spring Boot Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Spring Security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- JSON Web Token (JWT) -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2、创建JWT工具类:创建一个工具类,用于生成和解析JWT令牌。
package com.demo.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import java.util.Date;
import java.util.UUID;
/**
* JWT工具类
*/
public class JwtUtil {
//有效期为
public static final Long JWT_TTL = 24*60 * 60 *1000L;// 60 * 60 *1000 一个小时
//设置秘钥明文
public static final String JWT_KEY = "sangeng";
public static String getUUID(){
String token = UUID.randomUUID().toString().replaceAll("-", "");
return token;
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @return
*/
public static String createJWT(String subject) {
JwtBuilder builder = getJwtBuilder(subject, null, getUUID());// 设置过期时间
return builder.compact();
}
/**
* 生成jtw
* @param subject token中要存放的数据(json格式)
* @param ttlMillis token超时时间
* @return
*/
public static String createJWT(String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
return builder.compact();
}
private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
SecretKey secretKey = generalKey();
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
if(ttlMillis==null){
ttlMillis=JwtUtil.JWT_TTL;
}
long expMillis = nowMillis + ttlMillis;
Date expDate = new Date(expMillis);
return Jwts.builder()
.setId(uuid) //唯一的ID
.setSubject(subject) // 主题 可以是JSON数据
.setIssuer("sg") // 签发者
.setIssuedAt(now) // 签发时间
.signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
.setExpiration(expDate);
}
/**
* 创建token
* @param id
* @param subject
* @param ttlMillis
* @return
*/
public static String createJWT(String id, String subject, Long ttlMillis) {
JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);// 设置过期时间
return builder.compact();
}
public static void main(String[] args) throws Exception {
String token = "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiJjYWM2ZDVhZi1mNjVlLTQ0MDAtYjcxMi0zYWEwOGIyOTIwYjQiLCJzdWIiOiJzZyIsImlzcyI6InNnIiwiaWF0IjoxNjM4MTA2NzEyLCJleHAiOjE2MzgxMTAzMTJ9.JVsSbkP94wuczb4QryQbAke3ysBDIL5ou8fWsbt_ebg";
Claims claims = parseJWT(token);
System.out.println(claims);
}
/**
* 生成加密后的秘钥 secretKey
* @return
*/
public static SecretKey generalKey() {
byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 解析
*
* @param jwt
* @return
* @throws Exception
*/
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();
}
}
3、创建JWT认证过滤器:创建一个自定义的
JwtAuthenticationFilter
类,该类用于拦截和验证传入的请求中的JWT令牌。
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private JwtUtil jwtUtil;
public JwtAuthenticationFilter(JwtUtil jwtUtil, AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher("/api/**"));
this.jwtUtil = jwtUtil;
setAuthenticationManager(authenticationManager);
}
@Override
public org.springframework.security.core.Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
String token = extractTokenFromRequest(request);
if (token != null && jwtUtil.validateToken(token)) {
String username = jwtUtil.extractUsername(token);
org.springframework.security.core.Authentication auth = new UsernamePasswordAuthenticationToken(username, null);
return getAuthenticationManager().authenticate(auth);
}
return null;
}
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, org.springframework.security.core.Authentication authResult) throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
chain.doFilter(request, response);
}
private String extractTokenFromRequest(HttpServletRequest request) {
// 从请求中提取JWT令牌的逻辑,例如从请求头或请求参数中提取令牌。
return null;
}
}
4、配置Spring Security:创建一个Spring Security配置类,将JwtAuthenticationFilter和JwtUtil整合到Spring Security中。
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.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtUtil jwtUtil;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/api/login").permitAll()
.anyRequest().authenticated()
.and()
.addFilter(new JwtAuthenticationFilter(jwtUtil, authenticationManager()));
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 配置用户身份验证逻辑,例如从数据库中加载用户信息。
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
5、创建登录接口:创建一个登录接口,用于验证用户身份并生成JWT令牌。
@RestController
@RequestMapping("/api")
public class AuthController {
@Autowired
private JwtUtil jwtUtil;
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginRequest loginRequest) {
// 验证用户身份的逻辑,例如检查用户名和密码是否有效。
// 如果验证成功,生成JWT令牌并返回给客户端。
String token = jwtUtil.generateToken(loginRequest.getUsername());
return ResponseEntity.ok(token);
}
}