Springboot与SpringSecurity使用(1):介绍、登录验证

一、介绍

Spring 是非常流行和成功的 Java 应用开发框架,Spring Security 正是 Spring 家族中的成员。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是 SpringSecurity 重要核心功能。

Spring Security进行认证和鉴权的时候,就是利用的一系列的Filter来进行拦截的。

如图所示,一个请求想要访问到API就会从左到右经过虚线框里的过滤器,其中绿色部分是负责认证的过滤器,蓝色部分是负责异常处理,橙色部分则是负责授权。进过一系列拦截最终访问到我们的API。这里面重点关注两个过滤器:UsernamePasswordAuthenticationFilter负责登录认证,FilterSecurityInterceptor负责权限授权。

二、SpringSecurity入门

1、在项目中添加依赖

XML 复制代码
<dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

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

2、启动项目测试

在浏览器访问:http://localhost:8080/doc.html,自动跳转到了登录页面。

默认的用户名:user,密码在项目启动的时候在控制台会打印,注意每次启动的时候密码都会发生变化。

当输入错误的帐号密码时,页面会出现提示。

如果正确的话,则能正常访问:

说明Spring Security默认安全保护生效。在实际开发中,这些默认的配置是不能满足我们需要的,我们需要扩展Spring Security组件,完成自定义配置,实现我们的项目需求。

三、用户认证

用户认证流程:

  • Authentication接口: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。
  • AuthenticationManager接口:定义了认证Authentication的方法
  • UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
  • UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

1、用户认证核心组件

我们系统中会有许多用户,确认当前是哪个用户正在使用我们系统就是登录认证的最终目的。在Spring Security中的体现就是 Authentication,它存储了认证信息,代表当前登录用户。我们在程序中如何获取并使用它呢?我们需要通过 SecurityContext 来获取Authentication,SecurityContext就是我们的上下文对象。这个上下文对象则是交由 SecurityContextHolder 进行管理,你可以在程序任何地方使用它:

java 复制代码
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

Authentication中的信息:

  • Principal:用户信息,没有认证时一般是用户名,认证后一般是用户对象
  • Credentials:用户凭证,一般是密码
  • Authorities:用户权限

2、用户认证

AuthenticationManager 就是Spring Security用于执行身份验证的组件,只需要调用它的authenticate方法即可完成认证。Spring Security默认的认证方式就是在UsernamePasswordAuthenticationFilter这个过滤器中进行认证的,该过滤器负责认证逻辑。Spring Security用户认证关键代码如下:

java 复制代码
// 生成一个包含账号密码的认证信息
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(username, passwrod);
// AuthenticationManager校验这个认证信息,返回一个已认证的Authentication
Authentication authentication = authenticationManager.authenticate(authenticationToken);
// 将返回的Authentication存到上下文中
SecurityContextHolder.getContext().setAuthentication(authentication);

AuthenticationManager的校验逻辑非常简单:根据用户名先查询出用户对象(没有查到则抛出异常)将用户对象的密码和传递过来的密码进行校验,密码不匹配则抛出异常。其中使用了三个组件:

  • UserDetialsService接口只有一个方法loadUserByUsername(String username),通过用户名查询用户对象,默认实现是在内存中查询。
  • Spring Security中的用户数据则是由UserDetails来体现,该接口中提供了账号、密码等通用属性。
  • PasswordEncoder负责密码加密与校验。

UserDetialsService、UserDetails、PasswordEncoder,这三个组件Spring Security都有默认实现,这一般是满足不了我们的实际需求的,所以这里我们自己来实现这些组件。

加密器PasswordEncoder

java 复制代码
package com.ywz.security;

import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.util.DigestUtils;

import java.util.Arrays;

/**
 * 类描述 -> 自定义md5加密
 *
 * @Author: ywz
 * @Date: 2024/07/28
 */
public class CustomMd5PasswordEncoder implements PasswordEncoder {
    /**
     * 方法描述 -> 对密码进行md5加密
     *
     * @param rawPassword 未加密密码
     * @Return: @return {@link String }
     * @Author: ywz
     * @Date: 2024/07/28
     */
    @Override
    public String encode(CharSequence rawPassword) {
        return Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes()));
    }

    /**
     * 方法描述 -> 判断密码是否匹配
     *
     * @param rawPassword 未加密密码
     * @param encodedPassword 加密后的密码
     * @Return: @return boolean
     * @Author: ywz
     * @Date: 2024/07/28
     */
    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(Arrays.toString(DigestUtils.md5Digest(rawPassword.toString().getBytes())));
    }

}

用户对象UserDetails

该接口是用户对象,它提供了用户的一些通用属性,实际开发中我们的用户属性各种各样,这些默认属性可能是满足不了,所以我们一般会自己实现该接口,然后设置好我们实际的用户实体对象。实现此接口要重写很多方法比较麻烦,我们可以继承Spring Security提供的org.springframework.security.core.userdetails.User类,该类实现了UserDetails接口帮我们省去了重写方法的工作:

java 复制代码
package com.ywz.security;

import com.ywz.pojo.SysUser;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;

import java.util.Collection;

/**
 * 类描述 -> 自定义用户
 *
 * @Author: ywz
 * @Date: 2024/07/28
 */
@Setter
@Getter
public class CustomUser extends User {
    private SysUser sysUser;

    public CustomUser(SysUser sysUser, Collection<? extends GrantedAuthority> authorities) {
        super(sysUser.getUsername(), sysUser.getPassword(), authorities);
        this.sysUser = sysUser;
    }

}

业务对象UserDetailsService

该接口很简单只有一个方法,我们实现该接口,就完成了自己的业务:

java 复制代码
package com.ywz.security.service;

import com.ywz.pojo.SysUser;
import com.ywz.security.CustomUser;
import com.ywz.service.SysUserService;
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 javax.annotation.Resource;
import java.util.Collections;
import java.util.Objects;

/**
 * 类描述 -> 实现UserDetailsService接口,重写方法
 *
 * @Author: ywz
 * @Date: 2024/07/28
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService{
    @Resource
    private SysUserService sysUserService;
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserService.queryByUsername(username);
        if (Objects.isNull(sysUser)){
            throw new UsernameNotFoundException("用户名不存在!");
        }

        if(sysUser.getStatus() == 0) {
            throw new RuntimeException("账号已停用");
        }
        return new CustomUser(sysUser, Collections.emptyList());
    }
}

登录接口

接下需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。​ 认证成功的话要生成一个jwt,放入响应中返回。

java 复制代码
package com.ywz.controller;

import com.ywz.pojo.LoginVo;
import com.ywz.pojo.Result;
import com.ywz.service.SysUserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Map;

/**
 * 类描述 -> 登录控制器
 *
 * @Author: ywz
 * @Date: 2024/07/28
 */
@Api(tags = "系统管理-登录管理")
@RequestMapping("/admin/system/index")
@RestController
public class LoginController {
    @Resource
    private SysUserService sysUserService;

    /**
     * 方法描述 -> 登录接口
     *
     * @param loginVo -> 登录对象
     * @Return: @return {@link Result }<{@link Map }<{@link String },{@link Object }>>
     * @Author: ywz
     * @Date: 2024/07/28
     */
    @ApiOperation("登录接口")
    @PostMapping("/login")
    public Result<Map<String,Object>> login(@RequestBody LoginVo loginVo){
        return sysUserService.login(loginVo);
    }
}

SecurityConfig配置

java 复制代码
package com.ywz.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
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.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.Collections;

/**
 * 类描述 -> SpringSecurity配置类
 *
 * @Author: ywz
 * @Date: 2024/07/28
 */
@Configuration
@EnableWebSecurity // 是开启SpringSecurity的默认行为
public class SecurityConfig {

    /**
     * 方法描述 -> 密码明文加密方式配置
     *
     * @Return: @return {@link PasswordEncoder }
     * @Author: ywz
     * @Date: 2024/07/28
     */
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new CustomMd5PasswordEncoder();
    }

    /**
     * 方法描述 -> 获取AuthenticationManager(认证管理器),登录时认证使用
     *
     * @param authenticationConfiguration 认证配置
     * @Return: @return {@link AuthenticationManager }
     * @Author: ywz
     * @Date: 2024/07/28
     */
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }


    /**
     * 方法描述 -> 获取SecurityFilterChain(过滤器链),配置过滤器
     *
     * @param http httpSecurity
     * @Return: @return {@link SecurityFilterChain }
     * @Author: ywz
     * @Date: 2024/07/28
     */
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        return  http
                // 基于 token,不需要 csrf
                .csrf().disable()
                // 开启跨域以便前端调用接口
                .cors().and()
                .authorizeRequests()
                // 指定某些接口不需要通过验证即可访问。登录接口肯定是不需要认证的
                .antMatchers("/admin/system/index/login").permitAll()
                // 静态资源,可匿名访问
                .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()
                .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**","/doc.html").permitAll()
                // 这里意思是其它所有接口需要认证才能访问
                .anyRequest().authenticated()
                .and()
                // 基于 token,不需要 session
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
                // cors security 解决方案
                .cors().configurationSource(corsConfigurationSource())
                .and()
                .build();
    }

    /**
     * 方法描述 -> 配置跨源访问(CORS)
     *
     * @Return: @return {@link CorsConfigurationSource }
     * @Author: ywz
     * @Date: 2024/07/28
     */
    @Bean
    public CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedHeaders(Collections.singletonList("*"));
        configuration.setAllowedMethods(Collections.singletonList("*"));
        configuration.setAllowedOrigins(Collections.singletonList("*"));
        configuration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

}

执行登录

controller通过login方法调用实际业务

java 复制代码
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

    @Resource
    private SysMenuService sysMenuService;

    //通过AuthenticationManager的authenticate方法来进行用户认证,
    @Resource
    private AuthenticationManager authenticationManager;
    
	@Override
    public Result<Map<String, Object>> login(LoginVo loginVo) {
        // 将表单数据封装到 UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginVo.getUsername(), loginVo.getPassword());
        // authenticate方法会调用loadUserByUsername
        Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        if(Objects.isNull(authenticate)){
            throw new RuntimeException("用户名或密码错误");
        }
        // 校验成功,强转对象
        CustomUser customUser = (CustomUser) authenticate.getPrincipal();
        SysUser sysUser = customUser.getSysUser();
        // 校验通过返回token
        String token = JwtUtil.createToken(sysUser.getId(), sysUser.getUsername());
        Map<String, Object> map = new HashMap<>();
        map.put("token",token);
        return Result.ok(map);
    }
}

认证过滤器

我们需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的信息,获取对应的LoginUser对象。然后封装Authentication对象存入SecurityContextHolder。

java 复制代码
/**
 * 类描述 -> Jwt认证过滤器
 *
 * @Author: ywz
 * @Date: 2024/07/28
 */
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Autowired
    private RedisCache redisCache;

    /**
     * 方法描述 -> 在请求之前进行过滤
     *
     * @param request 请求
     * @param response 响应
     * @param filterChain 过滤器链
     * @Return:
     * @Author: ywz
     * @Date: 2024/07/28
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //获取token
        String token = request.getHeader("token");
        if (!StringUtils.hasText(token)) {
            //放行
            filterChain.doFilter(request, response);
            return;
        }
        //解析token
        String userid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            e.printStackTrace();
            throw new RuntimeException("token非法");
        }
        //从redis中获取用户信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录");
        }
        //存入SecurityContextHolder
        //TODO 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(loginUser,null,null);
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}
相关推荐
serve the people1 分钟前
springboot 单独新建一个文件实时写数据,当文件大于100M时按照日期时间做文件名进行归档
java·spring boot·后端
罗政5 小时前
[附源码]超简洁个人博客网站搭建+SpringBoot+Vue前后端分离
vue.js·spring boot·后端
拾光师7 小时前
spring获取当前request
java·后端·spring
xujinwei_gingko7 小时前
Spring IOC容器Bean对象管理-Java Config方式
java·spring
Java小白笔记8 小时前
关于使用Mybatis-Plus 自动填充功能失效问题
spring boot·后端·mybatis
小哇6669 小时前
Spring Boot,在应用程序启动后执行某些 SQL 语句
数据库·spring boot·sql
Xua30559 小时前
浅谈Spring Cloud:认识微服务
spring·spring cloud·微服务
JOJO___10 小时前
Spring IoC 配置类 总结
java·后端·spring·java-ee
luoluoal11 小时前
java项目之企业级工位管理系统源码(springboot)
java·开发语言·spring boot
蜜桃小阿雯11 小时前
JAVA开源项目 校园美食分享平台 计算机毕业设计
java·jvm·spring boot·spring cloud·intellij-idea·美食