【Spring】Spring Security 认证

Spring Security 认证

Spring Security 认证是指通过 Spring Security 框架提供的机制来验证用户的身份,确保用户是经过授权的合法用户。认证过程通常包括以下几个步骤:

1. 用户登录

  • 提交登录信息:用户通过登录表单提交用户名和密码等登录信息。
  • 过滤器处理:登录请求通常会被 UsernamePasswordAuthenticationFilter 过滤器捕获和处理。

2. 认证过程

  • 创建认证对象:过滤器会创建一个 UsernamePasswordAuthenticationToken 对象,包含用户的登录信息。
  • 调用认证管理器:将认证对象传递给 AuthenticationManager 进行认证。
  • 认证提供者处理AuthenticationManager 会委托给一个或多个 AuthenticationProvider 来处理具体的认证逻辑。例如,DaoAuthenticationProvider 会从用户数据存储中获取用户信息,并与提交的密码进行比对.
  • 认证结果
    • 成功:如果认证成功,AuthenticationProvider 会返回一个包含用户权限信息的 Authentication 对象。
    • 失败:如果认证失败,会抛出一个认证异常,如 BadCredentialsException 等.

3. 认证结果处理

  • 成功处理:如果认证成功,Authentication 对象会被存储到 SecurityContextHolder 中,表示用户已成功登录。此时,用户可以访问受保护的资源。
  • 失败处理:如果认证失败,通常会重定向到登录页面,并显示错误信息,提示用户重新登录.

4. 安全上下文

  • SecurityContextHolder:Spring Security 使用 SecurityContextHolder 来存储当前用户的认证信息。它是一个线程局部存储,确保每个请求都能访问到当前用户的认证信息.
  • SecurityContextSecurityContextHolder 中存储的是 SecurityContext 对象,它包含 Authentication 对象,表示当前用户的认证状态和权限信息.

5. 认证方式

Spring Security 支持多种认证方式,包括:

  • 表单登录:使用用户名和密码进行登录。
  • HTTP Basic Authentication:通过 HTTP 基本认证头进行认证。
  • OAuth2:使用 OAuth2 认证机制,如通过第三方服务进行认证.
  • JWT:使用 JSON Web Tokens 进行无状态认证等.

通过这些步骤和机制,Spring Security 能够有效地实现用户的身份验证和授权管理,确保系统的安全性.

1. 认证流程原理

1.1. 认证授权流程

SpringSecurity是基于Filter实现认证和授权,底层通过FilterChainProxy代理去调用各种Filter(Filter链),Filter通过调用AuthenticationManager完成认证 ,通过调用AccessDecisionManager完成授权。流程如下图:

在这里插入图片描述

1.2. Security过滤器链

我们知道,SpringSecurity是通过很多的过滤器链共同协作完成认证,授权的流程,SpringSecurity中核心的过滤器链如下:

在这里插入图片描述

SecurityContextPersistenceFilter

这个filter是整个filter链的入口和出口,请求开始会从SecurityContextRepository中获取SecurityContext对象并设置给SecurityContextHolder。在登录请求完成后将SecurityContextHolder持有的SecurityContext再保存到配置好的SecurityContextRepository中,同时清除SecurityContextHolder中的SecurityContext

UsernamePasswordAuthenticationFilter

默认拦截“/login”登录请求,处理表单提交的登录认证,将请求中的认证信息包括username,password等封装成UsernamePasswordAuthenticationToken,然后调用AuthenticationManager的认证方法进行认证。

BasicAuthenticationFilter

基本认证,httpBasic登录,弹出登录框登录

RememberAuthenticationFilter

记住我
RememberMeAuthenticationFilter 是 Spring Security 中用于实现“记住我”功能的一个过滤器。其主要作用是当用户没有登录而直接访问资源时,从 cookie 中获取用户信息,如果能够识别出用户提供的 remember me cookie,则用户无需重新输入用户名和密码,即可直接登录系统。

AnonymousAuthenticationFilter

匿名Filter,用来处理匿名访问的资源,如果SecurityContext中没有Authentication,就会创建匿名的Token(AnonymousAuthenticationToken),然后通过SecurityContextHodler设置到SecurityContext中。

ExceptionTranslationFilter

用来捕获FilterChain所有的异常,进行处理,但是只会处理 AuthenticationException和AccessDeniedException异常,其他的异常会继续抛出。

FilterSecurityInterceptor

用来做授权的Filter,通过父类(AbstractSecurityInterceptor.beforeInvocation)调用AccessDecisionManager.decide方法对用户进行授权。

1.3. Security相关概念

AuthenticationToken

所有提交给AuthenticationManager的认证请求都会被封装成一个Token的实现,比如最容易理解的UsernamePasswordAuthenticationToken,其中包含了用户名和密码。

AuthenticationManager

用户认证的管理类,所有的认证请求(比如login)都会通过提交一个token给AuthenticationManager的authenticate()方法来实现认证。AuthenticationManager会调用AuthenticationProvider.authenticate进行认证。认证成功后,返回一个包含了认证信息的Authentication对象。

AuthenticationProvider.authenticate

认证的具体实现类,一个provider是一种认证方式的实现,比如提交的用户名密码。我是通过和DB中查出的user记录做比对实现的,那就有一个DaoProvider;如果我是通过CAS请求单点登录系统实现,那就有一个CASProvider。按照Spring一贯的作风, 主流的认证方式它都已经提供了默认实现,比如DAO、LDAP、CAS、OAuth2等。 前面讲了AuthenticationManager只是一个代理接口,真正的认证就是由AuthenticationProvider来做的。一个AuthenticationManager可以包含多个Provider,每个provider通过实现一个support方法来表示自己支持那种Token的认证。AuthenticationManager默认的实现类是ProviderManager。

UserDetailService

用户的认证通过Provider来完成,而Provider会通过UserDetailService拿到数据库(或内存)中的认证信息然后和客户端提交的认证信息做校验。虽然叫Service,但是我更愿意把它认为是我们系统里经常有的UserDao。

SecurityContext

当用户通过认证之后,就会为这个用户生成一个唯一的SecurityContext,里面包含用户的认证信息Authentication。通过SecurityContext我们可以获取到用户的标识Principle和授权信息GrantedAuthrity。在系统的任何地方只要通过SecurityHolder.getSecruityContext()就可以获取到SecurityContext。在Shiro中通过SecurityUtils.getSubject()到达同样的目的

1.4. SpringSecurity认证流程原理

在这里插入图片描述
在这里插入图片描述

  • 请求过来会被过滤器链中的UsernamePasswordAuthenticationFilter拦截到,请求中的用户名和密码被封装成UsernamePasswordAuthenticationToken(Authentication的实现类)

  • 过滤器将UsernamePasswordAuthenticationToken提交给认证管理器(AuthenticationManager)进行认证。

  • AuthenticationManager委托AuthenticationProvider(DaoAuthenticationProvider)进行认证,AuthenticationProvider通过调用UserDetailsService获取到数据库中存储的用户信息(UserDetails),然后调用passwordEncoder密码编码器对UsernamePasswordAuthenticationToken中的密码和UserDetails中的密码进行比较

  • AuthenticationProvider认证成功后封装Authentication并设置好用户的信息(用户名,密码,权限等)返回

  • Authentication被返回到UsernamePasswordAuthenticationFilter,通过调用SecurityContextHolder工具把Authentication封装成SecurityContext中存储起来。然后UsernamePasswordAuthenticationFilter调用AuthenticationSuccessHandler.onAuthenticationSuccess做认证成功后续处理操作

  • 最后SecurityContextPersistenceFilter通过SecurityContextHolder.getContext()获取到SecurityContext对象然后调用SecurityContextRepository将SecurityContext存储起来,然后调用SecurityContextHolder.clearContext方法清理SecurityContext。

注意:SecurityContext是一个和当前线程绑定的工具,在代码的任何地方都可以通过SecurityContextHolder.getContext()获取到登陆信息。

2. 定义认证流程

2.1.添加依赖

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--        1.mybatis-plus整合SpringBoot的依赖-->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.4.1</version>
        </dependency>

        <!--        2.mysql的驱动依赖-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.23</version>
        </dependency>

		<dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

2.2 配置

2.2.1配置mybatis-plus,代码生成器生成对应代码。

2.2.2编写SpringSecurity配置类。

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
}

(1)定义密码编码器

案例中,密码一值是明文的,我们指定的密码编码器是 NoOpPasswordEncoder ,这个是不加密的,但是在生产环境中我们数据库中的密码肯定是密文,所以我们需要指定密码的编码器,SpringSecurity在认证时会调用我们指定的密码编码器进行认证。

BCryptPasswordEncoder是SpringSecurity内部提供的编码器,他的好处在于多次对相同的明文加密出来的密文是不一致的,但是多次加密出来的不同密文确有能检查通过,这种方式增加了密码的安全性,测试代码如下:

public class PasswordTest {
    @Test
    public void testPassword(){
        BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
        String enPass = bCryptPasswordEncoder.encode("123");
        System.out.println(enPass);
        System.out.println(bCryptPasswordEncoder.matches("123", enPass));
    }
}

在配置类中定义编码器如下:

@Bean
public PasswordEncoder passwordEncoder(){
    //return NoOpPasswordEncoder.getInstance();
    return new BCryptPasswordEncoder();
}

(2)授权规则配置

	//授权规则配置
   @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()                                //授权配置
                .antMatchers("/login").permitAll()              //登录路径放行
                .anyRequest().authenticated()                   //其他路径都要认证之后才能访问
                .and().formLogin()                               // 允许表单登录
//                .loginPage("/login.html").loginProcessingUrl("/login") // 自定义登录页面和登录逻辑路径
                .and().logout().logoutUrl("/logout").permitAll()    //自定义登出路径
                .and().csrf().disable();                        //关闭跨域伪造检查
    }

2.3. 定义UserDetailsService

  • UserDetailsService

是SpringSecurity提供用来获取认证用户信息(用户名,密码,用户的权限列表)的接口,我们可以实现该接口,复写loadUserByUsername(username)方法加载我们数据库中的用户信息。

  • UserDetails

UserDetails是SpringSecurity用来封装用户认证信息,权限信息的对象,我们使用它的实现类User封装用户信息并返回,我们这里从数据库查询用户名。

  • 创建类UserDetailServiceImpl实现UserDetailsService接口
/**
 * 用来提供给security的用户信息的service,
 * 我们需要复写 loadUserByUsername 方法返回数据库中的用户信息
 */
@Service
public class UserDetailServiceImpl implements UserDetailsService {
    /**
     * 加载数据库中的认证的用户的信息:用户名,密码,用户的权限列表
     * @param username: 该方法把username传入进来,我们通过username查询用户的信息
     (密码,权限列表等)然后封装成 UserDetails进行返回 ,交给security 。
     */
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        if (StringUtils.isBlank(username)){
            throw new UsernameNotFoundException("用户名不能为空");
        }
        LambdaQueryWrapper<SysUser> sysUserLambdaQueryWrapper = new LambdaQueryWrapper<>();
        sysUserLambdaQueryWrapper.eq(SysUser::getUsername,username);
        SysUser sysUser = sysUserMapper.selectOne(sysUserLambdaQueryWrapper);
        if (Objects.isNull(sysUser)){
            throw new UsernameNotFoundException("用户名不存在");
        }
        List<GrantedAuthority> permissions = new ArrayList<>();
        //密码是基于BCryptPasswordEncoder加密的密文
        //User是security内部的对象,UserDetails的实现类 ,用来封装用户的基本信息(用户名,密码,权限列表)
        return new User(username,sysUser.getPassword(),permissions);
    }
}

Provider会调用UserDetailsService 获取认证信息,这里自定义的UserDetailsService实现类,复写了loadUserByUsername方法,根据用户名查询数据库中的认证信息和当前用户的权限信息,封装成User返回。

**注意:**这里定义了UserDetailSerice后,WebSecurityConfig中不再需要定义UserDetailService的Bean,需要移除

2.4. 自定义登录逻辑

  1. 添加jwt依赖

    		<dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.1</version>
            </dependency>
    
  2. 导入jwt工具类

    package com.gcxy.utils;
    
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Component;
    
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * Jwt处理Token工具类
     *
     * @author fpf
     * @since 2024-12-23    
     */
    @Component
    public class JwtTokenUtil {
        /**
         * 用户名的key
         */
        private static final String CLAIM_KEY_USERNAME = "sub";
        /**
         * JWT的创建时间
         */
        private static final String CLAIM_KEY_CREATED = "created";
        /**
         * 秘钥
         */
        @Value("${jwt.secret}")
        private String secret;
        /**
         * token失效时间
         */
        @Value("${jwt.expiration}")
        private Long expiration;
    
        /**
         * 根据用户信息生成token
         *
    //     * @param userDetails 用户信息
         * @return 生成的token
         */
        public String generateToken(String username) {
            // 定义token中存储数据的载荷
            Map<String, Object> claims = new HashMap<>();
            // 1. 用户名
            claims.put(CLAIM_KEY_USERNAME, username);
            // 2. 签发时间
            claims.put(CLAIM_KEY_CREATED, new Date());
            // 签发token
            return generateToken(claims);
        }
    
        /**
         * 根据载荷生成token
         *
         * @param claims 载荷
         * @return 生成的token
         */
        private String generateToken(Map<String, Object> claims) {
            return Jwts.builder()
                    // 1. 设置载荷
                    .setClaims(claims)
                    // 2. 设置失效时间
                    .setExpiration(generateExpirationDate())
                    // 3. 设置签名
                    .signWith(SignatureAlgorithm.HS512, secret)
                    // 签发token
                    .compact();
        }
    
        /**
         * 生成token失效时间
         *
         * @return token的失效时间
         */
        private Date generateExpirationDate() {
            // token失效时间:当前系统时间 + 自己定义的时间
            return new Date(System.currentTimeMillis() + expiration * 1000);
        }
    
        /**
         * 从token中获取用户名
         *
         * @param token token
         * @return 当前token中存储的用户名
         */
        public String getUserNameFromToken(String token) {
            String userName;
            try {
                // 从token中获取到载荷
                Claims claims = getClaimsFromToken(token);
                // 通过载荷获取到用户名
                userName = claims.getSubject();
            } catch (Exception e) {
                // 如果出现异常则将 userName 设置为空
                userName = null;
            }
            return userName;
        }
    
        /**
         * 从token中获取载荷
         *
         * @param token token
         * @return token中的载荷
         */
        private Claims getClaimsFromToken(String token) {
            Claims claims = null;
            try {
                claims = Jwts.parser()
                        // 解密的秘钥
                        .setSigningKey(secret)
                        .parseClaimsJws(token)
                        .getBody();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return claims;
        }
    
        /**
         * 判断token是否可以被刷新
         *
         * @param token token
         * @return token是否可以被刷新
         */
        public boolean canRefresh(String token) {
            return !isTokenExpired(token);
        }
    
        /**
         * 判断token是否失效
         *
         * @param token token
         * @return 当前token是否失效
         */
        private boolean isTokenExpired(String token) {
            Date expiredDate = getExpiredDateFromToken(token);
            // 如果失效时间是在当前时间之前肯定是失效的
            return expiredDate.before(new Date());
        }
    
        /**
         * 获取token中的失效时间
         *
         * @param token token
         * @return 当前token中的失效时间
         */
        public Date getExpiredDateFromToken(String token) {
            // 从当前token中获取载荷
            Claims claims = getClaimsFromToken(token);
            // 从载荷中返回失效时间
            return claims.getExpiration();
        }
    
        /**
         * 刷新token
         *
         * @param token 用户携带的 token
         * @return 刷新后的 token
         */
        public String refreshToken(String token) {
            // 从当前token中获取载荷
            Claims claims = getClaimsFromToken(token);
            // 更新载荷中的失效时间改成当前时间
            claims.put(CLAIM_KEY_CREATED, new Date());
            // 重新生成token
            return generateToken(claims);
        }
    
        /**
         * 判断token是否有效
         *
         * @param token 用户携带的 token
         * @return token 是否有效
         */
        public boolean validateToken(String token, String username) {
            // 通过token是否已经过期、荷载中的用户属性与userDetails中的用户属性是否一致
            String userName = getUserNameFromToken(token);
            return userName.equals(username) && !isTokenExpired(token);
        }
    }
    
  3. 配置jwt对应参数

    jwt:
      secret: 123456
      expiration: 604800
    
  4. SpringSecurity配置类

    http.authorizeRequests()
                    // 放行登录接口
                    .antMatchers("/login").permitAll()
                    // 其他请求需要认证
                    .anyRequest().authenticated()
                    .and()
                    // 不通过Session获取SecurityContext
                    .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and().csrf().disable();
    
  5. controller中编写登录接口

    	@Autowired
        private SysUserServiceImpl sysUserService;
    
        //登录成功后重定向地址
        @RequestMapping("/login")
        @ResponseBody
        public String login(@RequestBody UserLoginDTO userLoginDTO){
            return sysUserService.login(userLoginDTO);
        }
    
  6. 编写参数对象UserLoginDTO

    package com.gcxy.dto;
    
    import io.swagger.annotations.ApiModel;
    import io.swagger.annotations.ApiModelProperty;
    import lombok.Data;
    import lombok.experimental.Accessors;
    
    /**
     * 用户登录参数
     *
     * @author fpf
     * @since 2024-12-23   
     */
    @Data
    @Accessors(chain = true)
    @ApiModel(value = "用户登录对象", description = "")
    public class UserLoginDTO {
    
        @ApiModelProperty(value = "用户名", required = true)
        private String username;
    
        @ApiModelProperty(value = "密码", required = true)
        private String password;
    }
    
  7. 编写登录的业务实现

    /**
     * <p>
     *  服务实现类
     * </p>
     *
     * @author fpf
     * @since 2024-12-23    */
    @Service
    public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements ISysUserService {
    
        @Autowired
        private UserDetailServiceImpl userDetailsService;
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        @Autowired
        private JwtTokenUtil jwtTokenUtil;
    
        public String login(UserLoginDTO userLoginDTO) {
            // 获取到UserDetails
            UserDetails userDetails = userDetailsService.loadUserByUsername(userLoginDTO.getUsername());
            // 如果userDetails为空或密码匹配不一致
            if (null == userDetails || !passwordEncoder.matches(userLoginDTO.getPassword(), userDetails.getPassword())) {
                throw new UsernameNotFoundException("用户名或密码错误");
            }
            String token = jwtTokenUtil.generateToken(userDetails.getUsername());
            // 登录成功后将token返回给前端
            return token;
        }
    }
    

2.5.Jwt过滤实现

  1. 定义JWT过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenUtil jwtTokenUtil;

    @Resource
    private UserDetailsService userDetailsService;

    @Override
    public void doFilterInternal(HttpServletRequest servletRequest, HttpServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
		
        // 跳过/login请求
        if ("/login".equals(servletRequest.getRequestURI())) {
            filterChain.doFilter(servletRequest, servletResponse);
            return;
        }
        // 1. 从当前请求中获取到授权数据
        String token = servletRequest.getHeader("Authorization");
        // 2. 判断获取到的请求头中是否存在token
        if (StringUtils.isBlank(token)) {
            responseWrite(servletResponse,"请携带token");
            return;
        }
        // 从token中获取到当前登录用户的用户名
        String username = jwtTokenUtil.getUserNameFromToken(token);
        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            // 执行登录操作
            UserDetails userDetails = userDetailsService.loadUserByUsername(username);
            if (jwtTokenUtil.validateToken(token, username)) {
                // 设置认证信息
                UsernamePasswordAuthenticationToken authenticationToken =
                        new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(servletRequest));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        }
        // 放行
        filterChain.doFilter(servletRequest, servletResponse);
    }

    private static void responseWrite(HttpServletResponse servletResponse,String message) throws IOException {
        // 设置编码格式
        servletResponse.setCharacterEncoding("UTF-8");
        servletResponse.setContentType("application/json");
        PrintWriter out = servletResponse.getWriter();
        out.write(message);
        out.flush();
        out.close();
    }
}

  1. SpringSecurity配置类中添加过滤器相关配置

    	@Autowired
        private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    
    //授权规则配置
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            ······
            http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        }
    

2.6. 自定义登出

SpringSecurity提供了默认的退出处理,可以在Security配置类中通过.and().logout().permitAll(); 使用默认的退出路径“/logout” ,如果我们需要自定义退出路径,可以通过如下方式指定:

.and().logout().logoutUrl("/mylogout").permitAll()    //自定义登出路径
.logoutSuccessHandler(new MyLogoutHandler())  //登出后处理器-可以做一些额外的事情
.invalidateHttpSession(true); //登出后session无效

如有侵权,联系删除

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值