SpringSecurity学习

2022-12-02 10:06:17 浏览数 (1)

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

0 人点赞