Spring Security学习笔记
这里使用的是springboot2.7.6+spring security+JWT0.12.6
1、导入依赖
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-api</artifactId><version>${jjwt.version}</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-impl</artifactId><version>${jjwt.version}</version>
</dependency>
<dependency><groupId>io.jsonwebtoken</groupId><artifactId>jjwt-jackson</artifactId><version>${jjwt.version}</version>
</dependency>
2、JwtUtil
使用jwt生成和解析token
package cn.darkiris.util;import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.stereotype.Component;import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;@Component
public class JwtUtil {// 这里的secretKey有长度要求,需要256字节private static final SecretKey secretKey = Keys.hmacShaKeyFor("xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx".getBytes());private static final long expiration = 1000L * 60 * 60 * 24 * 30;public String generateToken(String name, String password) {HashMap<String, Object> claims = new HashMap<>();claims.put("name", name);claims.put("password", password);return Jwts.builder().subject("JwtToken").issuer("Darkiris").claims(claims).expiration(new Date(System.currentTimeMillis() + expiration)).signWith(secretKey).compact();}public Claims parseToken(String token) {try {return Jwts.parser().verifyWith(secretKey).build().parseSignedClaims(token).getPayload();} catch (Exception e) {System.out.println(e.getMessage());return null;}}public String extractUsername(String token) {Claims claims = parseToken(token);if (claims == null) {return null;}return claims.get("name", String.class);}public boolean validateToken(String token) {Claims claims = parseToken(token);if (claims == null) {return false;}return claims.get("exp", Date.class).after(new Date());}
}
3、CustomUserDetail
这里的CustomUserDetail是继承UserDetails,作为CustomUserDetailService的loadUserByUsername的返回参数,供AuthenticationProvider使用
package cn.darkiris.security;import cn.darkiris.model.Permission;
import cn.darkiris.model.Role;
import cn.darkiris.model.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;import java.util.*;public class CustomUserDetail implements UserDetails {private final User user;private final Set<GrantedAuthority> authorities;public CustomUserDetail(User user) {this.user = user;this.authorities = getGrantedAuthority(user);System.out.println(this.authorities);}@Overridepublic Collection<? extends GrantedAuthority> getAuthorities() {return this.authorities;}@Overridepublic String getPassword() {return user.getPassword();}@Overridepublic String getUsername() {return user.getName();}@Overridepublic boolean isAccountNonExpired() {return true;}@Overridepublic boolean isAccountNonLocked() {return true;}@Overridepublic boolean isCredentialsNonExpired() {return true;}@Overridepublic boolean isEnabled() {return user.getStatus() == 0;}public Set<GrantedAuthority> getGrantedAuthority(User user) {Set<GrantedAuthority> grantedAuthorities = new HashSet<>();for (Role role : user.getRoles()) {grantedAuthorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));for (Permission permission : role.getPermissions()) {grantedAuthorities.add(new SimpleGrantedAuthority(permission.getModule() + ":" + permission.getOperation()));}}return grantedAuthorities;}
}
4、CustomUserDetailService
CustomUserDetailService是负责查DB获取用户信息和权限,返回就是我们上面自定义的CustomUserDetail,如果在这里对于用户信息你不需要动态判断用户是否过期、用户是否封禁、用户是否启用相关信息,可以简单直接使用org.springframework.security.core.userdetails.User
package cn.darkiris.security;import cn.darkiris.model.User;
import cn.darkiris.mapper.UserMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;@Component
public class CustomUserDetailService implements UserDetailsService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {User user = userMapper.selectUserByName(username);if (user == null) {System.out.println("UsernameNotFoundException: " + user);throw new UsernameNotFoundException(username);}return new CustomUserDetail(user);}
}
5、CustomPasswordEncoder
默认的PasswordEncoder是有一些自己的密码对比算法,我这边因为前端会处理密码的加密,不需要这里默认的PasswordEncoder的加密算法,所以需要自定义一下PasswordEncoder,完成密码的对比
package cn.darkiris.security;import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;@Component
public class CustomPasswordEncoder implements PasswordEncoder {@Overridepublic String encode(CharSequence rawPassword) {return rawPassword.toString();}@Overridepublic boolean matches(CharSequence rawPassword, String encodedPassword) {return encodedPassword.equals(rawPassword.toString());}
}
6、CustomAuthenticationEntryPoint
这里我们因为要规范化接口返回值,所以自定义鉴权失败处理
package cn.darkiris.security;import cn.darkiris.entity.ResponseEntity;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);response.getWriter().write(objectMapper.writeValueAsString(ResponseEntity.UnAuthentication("authentication failed")));}
}
7、CustomAccessDeniedHandler
这里我们因为要规范化接口返回值,所以自定义授权失败处理
package cn.darkiris.security;import cn.darkiris.entity.ResponseEntity;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {@Autowiredprivate ObjectMapper objectMapper;@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.setContentType("application/json;charset=UTF-8");response.setStatus(HttpServletResponse.SC_FORBIDDEN);response.getWriter().write(objectMapper.writeValueAsString(ResponseEntity.UnAuthorization("access denied")));}
}
8、CustomAuthenticationProvider
默认的AuthenticationProvider内部处理了异常,隐藏了UsernameNotFoundException异常,但是我们这里需要给予用户有好的提示,所以自定义了AuthenticationProvider
package cn.darkiris.security;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {@Autowiredprivate CustomUserDetailService customUserDetailService;@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {String name = authentication.getName();String password = (String) authentication.getCredentials();UserDetails userDetails = customUserDetailService.loadUserByUsername(name);if (userDetails == null) {throw new UsernameNotFoundException(name);}if (!password.equals(userDetails.getPassword())) {throw new BadCredentialsException(name);}return new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());}@Overridepublic boolean supports(Class<?> authentication) {return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);}
}
9、SecurityConfig
配置security
package cn.darkiris.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.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class SecurityConfig {@Autowiredprivate CustomAuthenticationEntryPoint customAuthenticationEntryPoint;@Autowiredprivate CustomAccessDeniedHandler customAccessDeniedHandler;@Autowiredprivate JwtAuthenticationFilter jwtAuthenticationFilter;@Beanpublic AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {// 1、AuthenticationManager 委托给适当的 AuthenticationProvider(CustomAuthenticationProvider)// 2、AuthenticationProvider 调用 UserDetailsService(CustomUserDetailService) 的 loadUserByUsername 获取用户信息// 3、AuthenticationProvider 使用 PasswordEncoder(CustomPasswordEncoder) 对比 上一步获取到的用户密码// 4、如果验证成功,AuthenticationProvider(CustomAuthenticationProvider) 返回一个已认证的 Authentication 对象;如果失败,抛出异常return authenticationConfiguration.getAuthenticationManager();}@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.csrf().disable() // 关闭csrf防护.exceptionHandling() // 自定义异常处理.authenticationEntryPoint(customAuthenticationEntryPoint) //自定义鉴权失败处理.accessDeniedHandler(customAccessDeniedHandler) // 自定义授权失败.and().sessionManagement() // 关闭session.sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests() // 需要鉴权的请求路径.antMatchers("/api/user/login", "/api/user/register").permitAll() // 放行登录接口.antMatchers("/v3/api-docs/**", "/swagger-ui/**", "/swagger-resources/**", "/webjars/**").permitAll() // 放行接口文档.anyRequest().authenticated() // 其他请求需要鉴权.and().addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); //使用自定义JWT过滤器return http.build();}
}