SpringSecurity学习
介绍
spring security 的核心功能主要包括:
- 用户认证(是否有登录用户)
- 授权(授权用户权限,能做些什么)
- 攻击防护(防止身份伪造,防范 CSRF 攻击 )
其核心就是一组过滤器链,项目启动后将会自动配置。最核心的就是 Basic Authentication Filter 用来认证用户的身份,一个在spring security中一种过滤器处理一种认证方式
整合SpringBoot
加入依赖
代码语言:javascript复制<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
没有 Spring Boot 的 Maven
代码语言:javascript复制<dependencyManagement>
<dependencies>
<!-- ... other dependency elements ... -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-bom</artifactId>
<version>{spring-security-version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
以下是 Spring Security Filter 排序的完整列表:
ForceEagerSessionCreationFilter
- ChannelProcessingFilter
- WebAsyncManagerIntegrationFilter
- SecurityContextPersistenceFilter
- HeaderWriterFilter
- CorsFilter
- CsrfFilter
- LogoutFilter
- OAuth2AuthorizationRequestRedirectFilter
- Saml2WebSsoAuthenticationRequestFilter
- X509AuthenticationFilter
- AbstractPreAuthenticatedProcessingFilter
- CasAuthenticationFilter
- OAuth2LoginAuthenticationFilter
- Saml2WebSsoAuthenticationFilter
UsernamePasswordAuthenticationFilter
- OpenIDAuthenticationFilter
- DefaultLoginPageGeneratingFilter
- DefaultLogoutPageGeneratingFilter
- ConcurrentSessionFilter
DigestAuthenticationFilter
- BearerTokenAuthenticationFilter
BasicAuthenticationFilter
- RequestCacheAwareFilter
- SecurityContextHolderAwareRequestFilter
- JaasApiIntegrationFilter
- RememberMeAuthenticationFilter
- AnonymousAuthenticationFilter
- OAuth2AuthorizationCodeGrantFilter
- SessionManagementFilter
ExceptionTranslationFilter
FilterSecurityInterceptor
- SwitchUserFilter
Spring Security 在 Servlet 身份验证中使用的主要架构组件
- SecurityContextHolder - 的
SecurityContextHolder
是 Spring Security 存储 身份验证 。 - SecurityContext - 从
SecurityContextHolder
并包含Authentication
当前已通过身份验证的用户。 - Authentication - 可以作为输入
AuthenticationManager
提供用户提供的凭据以进行身份验证或来自当前用户SecurityContext
. - GrantedAuthority - 在
Authentication
(即角色、范围等) - AuthenticationManager - 定义 Spring Security 的过滤器如何执行身份验证的 API 。
- ProviderManager - 最常见的实现
AuthenticationManager
. - AuthenticationProvider - 由
ProviderManager
执行特定类型的身份验证。 - Request Credentials with
AuthenticationEntryPoint
用于从客户端请求凭据(即重定向到登录页面,发送WWW-Authenticate
回复等) - AbstractAuthenticationProcessingFilter - 一个基础
Filter
用于身份验证。 这也很好地了解了身份验证的高级流程以及各个部分如何协同工作。
SecurityContextHolder上下文持有者
这 SecurityContextHolder
是 Spring Security 存储 身份验证 。 Spring Security 并不关心如何 SecurityContextHolder
被填充。 如果它包含一个值,则将其用作当前经过身份验证的用户。
示例:
代码语言:javascript复制SecurityContext context = SecurityContextHolder.createEmptyContext();
Authentication authentication =
new TestingAuthenticationToken("username", "password", "ROLE_USER");
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
java
- 我们首先创建一个空的 SecurityContext. 你应该创建一个新的 SecurityContext实例而不是使用 SecurityContextHolder.getContext().setAuthentication(authentication)避免跨多个线程的竞争条件。
- 接下来,我们新建一个 Authentication目的。 Spring Security 不在乎什么类型 Authentication实施设置在 SecurityContext. 在这里,我们使用 TestingAuthenticationToken,因为它非常简单。
- 更常见的生产场景是 UsernamePasswordAuthenticationToken(userDetails, password, authorities).最后,我们设置 SecurityContext在 SecurityContextHolder. Spring Security 使用此信息进行授权
要获取有关经过身份验证的主体的信息,请访问上下文持有者
访问当前经过身份验证的用户
代码语言:javascript复制SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
//用户name
String username = authentication.getName();
//用户基本信息
Object principal = authentication.getPrincipal();
//获取用户角色
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
java
Authentication 验证
这 Authentication
接口在 Spring Security 中有两个主要用途:
- 一个输入
AuthenticationManager
提供用户提供的身份验证凭据。 在这种情况下使用时,isAuthenticated()
返回false
. - 表示当前经过身份验证的用户。 您可以获得当前
Authentication
从 SecurityContext 。
这 Authentication
包含:
principal
: 标识用户。 使用用户名/密码进行身份验证时,这通常是UserDetails
.credentials
: 通常是密码。 在很多情况下,这在用户通过身份验证后会被清除,以确保它不被泄露。authorities
: 这GrantedAuthority
实例是授予用户的高级权限。 两个例子是角色和范围。
具体实现
现阶段理解主要流程,首先配置SpringSecurity的配置类。
SecurityConfig.java
代码语言:javascript复制package com.example.springstudy.config;
import com.example.springstudy.filter.JWTAuthenticationTokenFilter;
import com.example.springstudy.handler.AccessDeniedHandlerImpl;
import com.example.springstudy.handler.AuthenticationPointImpl;
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.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @Author sunjl
* @Date 2022/7/5 17:24
* @Version 1.0.0
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JWTAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationPointImpl authenticationPoint;
@Autowired
private AccessDeniedHandlerImpl accessDeniedHandler;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 配置放行
*
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
// http.csrf().disable()
// //不通过session获取SecurityContext
// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// .and()
// .authorizeRequests()
// //登录接口允许匿名访问
// .antMatchers("/user/login").anonymous()
//// 拦截其他接口
// .anyRequest()
// .authenticated();
//将自定义的拦截器放在Sercurity拦截器中的UsernamePasswordAuthenticationFilter之前
http.csrf().disable().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().authorizeRequests()
//没登录也可以访问
.antMatchers("/user/login").anonymous()
//其他请求认证之后可以访问
.anyRequest().authenticated();
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
// 配置异常处理器
http.exceptionHandling().authenticationEntryPoint(authenticationPoint).accessDeniedHandler(accessDeniedHandler);
http.cors();
}
}
java
当用户请求登录,发送用户名和密码到服务器,通过过滤器,到UserDetailService的实现类中,通过用户名判断是否存在当前用户,存在当前用户同时将当前用户的角色和信息封装到一个UserDtailsService类中并返回。
UserDetailServiceImpl.java
代码语言:javascript复制package com.example.springstudy.service.Impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.example.springstudy.entity.LoginUser;
import com.example.springstudy.entity.User;
import com.example.springstudy.mapper.MenuMapper;
import com.example.springstudy.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
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.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;
/**
* @Author sunjl
* @Date 2022/7/5 14:42
* @Version 1.0.0
*/
@Slf4j
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Autowired
MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
//new QueryWrapper 不能直接使用Lambad表达式
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
log.info(name "-=======");
queryWrapper.eq(User::getUserName,name);
User user = userMapper.selectOne(queryWrapper);
if (Objects.isNull(user)){
throw new RuntimeException("用户名或密码错误");
}
//查询用户权限
List<String> userRole = menuMapper.selectUserRole(user.getId());
// List<String> userRole=new ArrayList<>(Arrays.asList("test"));
return new LoginUser(user,userRole);
}
}
java
整合JWT
JWT工具类
JwtUtil.java
代码语言:javascript复制package com.example.springstudy.utills;
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 = 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 jwt = createJWT("2123");
Claims claims = parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyOTY2ZGE3NGYyZGM0ZDAxOGU1OWYwNjBkYmZkMjZhMSIsInN1YiI6IjIiLCJpc3MiOiJzZyIsImlhdCI6MTYzOTk2MjU1MCwiZXhwIjoxNjM5OTY2MTUwfQ.NluqZnyJ0gHz-2wBIari2r3XpPp06UMn4JS2sWHILs0");
String subject = claims.getSubject();
System.out.println(subject);
// 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();
}
}
java
配置SpringSecurity拦截器
代码语言:javascript复制package com.example.springstudy.filter;
import com.alibaba.fastjson.JSON;
import com.example.springstudy.entity.LoginUser;
import com.example.springstudy.utills.JwtUtil;
import com.example.springstudy.utills.RedisCache;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Objects;
/**
* @Author sunjl
* @Date 2022/7/6 9:40
* @Version 1.0.0
*/
@Slf4j
@Component
public class JWTAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = httpServletRequest.getHeader("token");
if (!StringUtils.hasText(token)) {
filterChain.doFilter(httpServletRequest, httpServletResponse);
return;
}
//解析token
String userId;
try {
Claims claims = JwtUtil.parseJWT(token);
userId = claims.getSubject();
log.info("userId====" userId);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("token非法");
}
//获取用户信息
LoginUser loginUser = redisCache.getCacheObject("userInfo_" userId);
log.info("权限信息:" loginUser.getAuthorities());
// LoginUser loginUser = JSON.parseObject(JSON.toJSONString(o), LoginUser.class);
if (Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//TODO 获取权限信息封装到Authentication中
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
//将用户信息存放在SecurityContext中
// SecurityContextHolder.getContext().setAuthentication(authenticationToken);
securityContext.setAuthentication(authenticationToken);
filterChain.doFilter(httpServletRequest,httpServletResponse);
}
}
java
配置的拦截器需要在配置类中配置拦截器的信息。
其他SpringSecurity中自带的用户的未登录(AuthenticationEntryPoint)、权限不足(AccessDeniedHandler)、登录失败(AuthenticationFailureHandler)、登录成功(AuthenticationSuccessHandler)、退出登录(LogoutSuccessHandler)的配置
例子:
没有权限:
代码语言:javascript复制@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(new Result(false, StatusConst.AUTHORIZED,"没有操作权限")));
}
}
java
未登录
代码语言:javascript复制@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
httpServletResponse.setContentType("application/json;charset=utf-8");
httpServletResponse.getWriter().write(JSON.toJSONString(new Result(false, StatusConst.NOT_LOGIN, "请登录")));
}
}
java
登录失败
代码语言:javascript复制@Component
public class AuthenticationFailHandlerImpl implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(JSON.toJSONString(new Result(false, StatusConst.ERROR,e.getMessage())));
}
}
java
登录成功
代码语言:javascript复制@Component
public class AuthenticationSuccessHandlerImpl implements AuthenticationSuccessHandler {
@Autowired
private UserAuthDao userAuthDao;
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException {
// 更新用户ip,最近登录时间
updateUserInfo();
UserLoginDTO userLoginDTO = BeanCopyUtil.copyObject(UserUtil.getLoginUser(), UserLoginDTO.class);
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(JSON.toJSONString(new Result<UserInfoDTO>(true, StatusConst.OK, "登录成功!", userLoginDTO)));
}
}
java
注销
代码语言:javascript复制@Component
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.getWriter().write(JSON.toJSONString(new Result(true, StatusConst.OK,"注销成功")));
}
}
java
代码语言:javascript复制run.getBean(DefaultSecurityFilterChain.class)
java