SpringBoot 整合SpringSecurity认证

简介

Spring Security 是Spring家族中的一个安全管理框架。相比与另外一个安全框架shiro,它提供了更丰富的功能,社区资源也比Shiro丰富。

—般Web应用的需要进行认证授权

  • 认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户
  • 授权:经过认证后判断当前用户是否有权限进行某个操作

而认证和授权也是SpringSecurity作为安全框架的核心功能。

1、初步使用

准备工作

搭建一个简单的SpringBoot工程,在该工程需要引入web服务,其pom.xml如下:

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

创建页面

创建一个HelloController的类,类内容如下:

@RestController
@RequestMapping("hello")
public class HelloController {

    @GetMapping
    public String hello(){
        return "hello world";
    }
}

引入Spring Security依赖

在pom.xml中引入Spring Security依赖,如下:

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

启动应用,并在浏览器访问:http://localhost:8080/hello的接口时,系统会自动跳转到SpringSecurity默认的登录页面,默认的用户名为: user,密码会输出在控制台,如下:

SpringBoot 整合SpringSecurity认证
SpringBoot 整合SpringSecurity认证

初始帐号号:user

默认退出接口:/logout

认证原理

SpringSecurity的认证原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器,如下图所示:

SpringBoot 整合SpringSecurity认证

图中展示了SpringSecurity的核心认证过滤器

UsernamePasswordAuthenticationFilter:(负责认证)负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。

ExceptionTranslationFilter: 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。

FilterSecurityInterceptor:负责权限校验的过滤器。

认证流程

Authentication: 它的实现类,表示当前访问系统的用户,封装了用户相关信息。

AuthenticationManager: 定义了认证Authentication的方法

UserDetailsService: 加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。

UserDetails: 提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

2、使用JWT自定义认证

思路分析

登录:

1、自定义登录接口,调用ProviderManager的方法进行认证,如果认证通过生成jwt,同时把用户信息存入redis中。

2、自定义UserDetailService:在这个实现类中查询数据库

校验:

1.定义JWT认证过滤器

2、获取Token并解析,获取token中的userid,根据userid从redis中获取用户信息存入SecurityContextHolder中,然后其它的过滤器会从SecurityContextHolder中获取用户信息。

引入JWT依赖

<!-- 引入JWT -->
<dependency>
   <groupId>io.jsonwebtoken</groupId>
   <artifactId>jjwt</artifactId>
   <version>0.9.0</version>
</dependency>
<!-- 引入redis依赖 -->
<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入fastjson -->
<dependency>
   <groupId>com.alibaba</groupId>
   <artifactId>fastjson</artifactId>
   <version>1.2.75</version>
</dependency>

配置信息

在application.yml配置文件新增redis连接的相关连接,如下:

spring:
  redis:
    host: 127.0.0.1
    port: 6379

编写工具类

JWT工具类

public class JwtUtil {

    //有效期
    public static final Long JWT_TTL = 60*60*1000L;

    //设置密钥明文
    public static final String JWT_KEY = "tech";

    public static String getUUID(){
        String token = UUID.randomUUID().toString().replace("-", "");
        return token;
    }

    /**
     * 生成JWT
     * @param subject
     * @return
     */
    public static String createJWT(String subject){
        JwtBuilder builder = getJwtBuilder(subject, JWT_TTL, getUUID());
        return builder.compact();
    }

    /**
     * 解析JWT
     * @param jwt
     * @return
     */
    public static Claims parseJWT(String jwt){
        SecretKey secretKey = generalKey();
        return Jwts.parser().setSigningKey(secretKey).parseClaimsJws(jwt).getBody();
    }

    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 = JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        JwtBuilder builder = Jwts.builder()
                .setId(uuid) // 唯一的ID
                .setSubject(subject)  // 主题,可以是JSON数据
                .setIssuer("techlearn") //签发者
                .setIssuedAt(now) //签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);//过期时间
        return builder;
    }


    /**
     * 生成加密后的密钥 secretKey
     * @return
     */
    public static SecretKey generalKey(){
        byte[] encodedKey = JWT_KEY.getBytes();
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
}

统一响应类

@Data
public class ResponseResult <T>{
    /**
     * 状态码
     */
    private Integer code;
    /**
     * 提交信息
     */
    private String msg;
    /**
     * 结果信息
     */
    private T data;

    public ResponseResult(Integer code, String msg){
        this.code = code;
        this.msg = msg;
    }

    public ResponseResult(Integer code, T data){
        this.code = code;
        this.data = data;
    }

    public ResponseResult(Integer code, String msg, T data){
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

返回Response工具类

public class ResponseUtil {

    public static void out(HttpServletResponse response, ResponseResult result){
        response.setStatus(HttpStatus.OK.value());
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        response.setCharacterEncoding("UTF-8");
        ObjectMapper mapper = new ObjectMapper();
        PrintWriter writer = null;
        try {
            writer = response.getWriter();
            mapper.writeValue(writer, result);
            writer.flush();
        } catch (IOException e) {
            e.printStackTrace();
        }  finally {
            if(writer != null){
                writer.flush();
                writer.close();
            }
        }
    }
}

登录用户类

注意,该类需要实现UserDetails类,这样会方便后面的认证授权操作

@Data
public class LoginUser implements UserDetails {
    private String uuid;
    private String username;
    private String password;

    // 存储权限信息
    private List<String> permissions;

    private Set<GrantedAuthority> authorities;

    public LoginUser(String username, String password, List<String> permissions){
        this.username = username;
        this.password =  password;
        this.permissions = permissions;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(Objects.nonNull(authorities)){
            return authorities;
        }
        // 把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities中
        authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toSet());;
        return authorities;
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

SpringSecurity登录

实现SpringSecurity自定认证主要有以下几个步骤:

1、创建一个类实现UserDetailsService接口,并重写loadUserByUsername方法,该方法是根据用户名查询到用户信息,比如用户名,密码,权限信息等。

2、创建访问拦截过滤器,该过滤器需要实现Security中的OncePerRequestFilter类,在该类中主要对所有访问的进行拦截过滤,并将用户权限之类的信息封将存入SecurityContextHolder中让SpringSecurity进行认证操作。

3、定义登录认证失败的处理器,该处理器需要实现AuthenticationEntryPoint接口,该类需要对用户名密码验证失败进行处理,并返回统一的内容供前端使用。

4、定义无权限访问处理器,该处理器需要实现AccessDeniedHandler接口,该类主要处理对于无访问权限进行处理,并返回统一的内容供前端使用。

5、将2、3、4步自定义的类配置到SpringSecurity认证中,从而替代SpringSecurity中默认的登录认证逻辑。

1、实现UserDetailService接口

创建一个UserServiceImpl类,该类需要实现UserDetailService接口,如下:

@Service
public class UserServiceImpl implements UserDetailsService {

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException 		{
        // TODO 根据用户名查询用户信息,这里使用假数据进行操作
        if(!username.equals("test")){
            throw new RuntimeException("用户名错误!!!");
        }
        String password = passwordEncoder.encode("123456");
        // TODO 查询相应的权限信息,这里使用假数据进行测试
        List<String> permissions = new ArrayList<>(Arrays.asList("hello", "admin"));

        // 将数据封装成UserDetails对象返回
        return new LoginUser(username, password, permissions);
    }
}

2、定义访问过滤器

定义JwtAuthenticationTokenFilter过滤器,该过滤器需要继承OncePerRequestFilter类,记的将JwtAuthenticationTokenFilter注入到Spring容器中,如下:

@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {

    @Resource
    private RedisTemplate redisTemplate;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 获取请求头中的token
        String token = request.getHeader("token");
        // token为空则放行
        if(!StringUtils.hasText(token)){
            System.out.println("token 为空");
            // 放行
            filterChain.doFilter(request, response);
            return;
        }
        // 解析token
        String uuid;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            uuid = claims.getSubject();
        }catch (Exception e){
            throw new RuntimeException("Token 非法");
        }

        String redisKey = "login_"+uuid;
        LoginUser loginUser = JSON.parseObject((String) redisTemplate.opsForValue().get(redisKey), LoginUser.class);
        if(Objects.isNull(loginUser)){
            throw new RuntimeException("用户未登录!!!");
        }
        // 获取权限信息封装到Authentication中
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
        // 将权限与认证信息存入SecurityContextHolder
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        //放行
        filterChain.doFilter(request, response);
    }
}

3、定义认证失败处理器

定义登录认证失败处理器SecurityAuthenticationEntryPoint,该类需要实现AuthenticationEntryPoint接口,如下:

@Component
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        System.out.println("认证有误");
        ResponseUtil.out(response, new ResponseResult(400, authException.getMessage()));
    }
}

4、定义权限访问失败处理器

定义权限访问失败处理器AccessDeniedHandlerImpl,该类处理无访问权限问题,如下:

@Component
public class AccessDeniedHandlerImpl implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        System.out.println("无访问权限");
        ResponseUtil.out(response, new ResponseResult<>(500, "无访问权限"));
    }
}

5、跨域

浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。 同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。

新建CorsConfig配置类,该类需要实现WebMvcConfigurer接口,如下:

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry){
        // 设置允许跨域的路径
        registry.addMapping("/**")
                 // 设置允许跨域请求的域名
                .allowedOriginPatterns("*")
                // 是否允许cookie
                .allowCredentials(true)
                // 设置允许的请求方式
                .allowedMethods("GET", "POST", "DELETED", "PUT", "OPTIONS")
                // 设置允许的header属性
                .allowedHeaders("*")
                // 跨域允许的时间
                .maxAge(3600);
    }
}

6、配置自定义至SpringSecurity

自定义一个SecurityConfig的配置类,该类主要进行SpringSecurity的登录认证,权限等配置,如下:

@Configuration
public class SecurityConfig {

    @Autowired
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;

    @Autowired
    private SecurityAuthenticationEntryPoint securityAuthenticationEntryPoint;

    @Autowired
    private AccessDeniedHandlerImpl accessDeniedHandler;

    /**
     * 更改SpringSecurity的密码加密方式为BCryptPasswordEncoder
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity
                // CSRF 禁用
                .csrf().disable()
                // 不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                // 过滤请求
                .authorizeRequests()
                // 对于登录接口,允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除了上面外的所有请求全都需要鉴权认证
                .anyRequest().authenticated();
        //把Token校验过滤器添加到过滤器链中
        httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
        httpSecurity.exceptionHandling()
                // 配置认证失败处理器
                .authenticationEntryPoint(securityAuthenticationEntryPoint)
                //  配置授权访问失败处理器
                .accessDeniedHandler(accessDeniedHandler);
      	// 允许跨域
        httpSecurity.cors();
        return httpSecurity.build();
    }
}

测试

1、新建LoginService

@Service
public class LoginServiceImpl implements LoginService {

    @Resource
    private AuthenticationManager authenticationManager;

    @Autowired
    private RedisTemplate redisTemplate;

    @Override
    public String login(User user) {
        // 使用AuthenticationManager的authenticate方法进行用户认证
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword());
        Authentication authentication = authenticationManager.authenticate(authenticationToken);
        // 如果认证没有通过,给出相应的提示
        if(Objects.isNull(authentication)){
            throw new RuntimeException("认证失败");
        }
        // 如果认证通过了,使用UUID(userid)生成JWT
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        String uuid = loginUser.getUuid();
        String token = JwtUtil.createJWT(uuid);
        loginUser.setUuid(uuid);
        // 将完整的用户信息存入redis
        redisTemplate.opsForValue().set("login_"+uuid, JSON.toJSONString(loginUser));
        return token;
    }

    @Override
    public String logout() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        redisTemplate.delete("login_"+loginUser.getUuid());
        return null;
    }
}

2、新建LoginController

@RestController
@RequestMapping("user")
public class LoginController {

    @Autowired
    private LoginService loginService;

    @GetMapping("/logout")
    public ResponseResult logout(){
        loginService.logout();
        return new ResponseResult(200, "登出成功");
    }

    @GetMapping("/login")
    public ResponseResult login(){
        User user = new User();
        user.setUsername("test");
        user.setPassword("123456");
        String token = loginService.login(user);
        return new ResponseResult(200, token);
    }
}

启动应用,访问http://localhost:8080/user/login接口,则可以得到token,如下:

{
 "code":200,
 "msg":"eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2YjhiZGEyMTczMTQ0MGQyYTMwN2Y3NzVhZTQ3YzEwNSIsImlzcyI6InRlY2hsZWFybiIsImlhdCI6MTY3MDY4Mjc0NSwiZXhwIjoxNjcwNjg2MzQ1fQ.ZBt1POGx3gy0eyGaHbBuIcmtDYZ4ITvgzEZKkv2eHCk",
  "data":null
}

4、新建HelloController

@RestController
@RequestMapping("hello")
public class HelloController {

    // 定义访问这个方法需要'hello'权限
    @PreAuthorize("hasAnyAuthority('hello')")
    @GetMapping("/{id}")
    public ResponseResult hello(@PathVariable Integer id) {
        return new ResponseResult(200, "hello world");
    }

  	// 定义访问这个方法需要'world'权限
    @PreAuthorize("hasAnyAuthority('world')")
    @GetMapping("world")
    public ResponseResult world(){
        return new ResponseResult(200, "这个不需权限即可访问!!!");
    }
}

5、开启权限访问

在启动类中,添加EnableGlobalMethodSecurity注解用来开启注解权限,如下:

@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecuritydemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(SecuritydemoApplication.class, args);
    }
}

启动应该,访问:http://localhost:8080/hello,记的需要在header中添加token信息,如下:

SpringBoot 整合SpringSecurity认证

原创文章,作者:jiafegn,如若转载,请注明出处:https://www.techlearn.cn/archives/1989

Previous 2023年12月17日
Next 2024年1月11日

相关推荐

  • SpringBoot 整合Mybatis-Plus

    简介 MyBatis-Plus是一个MyBatis的增强工具,在Mybatis的基础上只做增强不做改变,为简化开发,提高效率而生。 添加依赖 注意:添加Mybatis-Plus即可…

    springboot 2023年3月26日
    131
  • SpringBoot 过滤器

    简介 SpringBoot过滤器在web开发中可以过滤指定的URL,比如拦截掉不需要的接口请求,同时也可以对request和response的内容进行修改。 使用场景 Spring…

    springboot 2023年4月30日
    240
  • SpringBoot 注解 @Autowired

    作用 实现依赖注入查找方式:@Autowired是先到容器查找类型,如果该类型只有一个那么就直接注入,有多个时再根据名字找使用范围:构造器、方法、参数、字段、注解参数:requir…

    springboot 2022年9月9日
    404
  • SpringBoot Spring Event 业务解耦神器

    介绍 Spring Event是Spring框架中的一个事件机制,主要用于实现应用程序内部的事件传递与处理,它允许不同组件之间通过发布-订阅机制进行解耦通信,比如用户注册,订单创建…

    springboot 2024年1月11日
    301
  • SpringBoot拦截器

    简介 拦截器可以根据 URL 对请求进行拦截,主要应用于登陆校验、权限验证、乱码解决、性能监控和异常处理等功能。 使用步骤 在SpringBoot中使用拦截器功能通常需要以下3步:…

    springboot 2023年4月29日
    123
  • SpringBoot 整合Memcached

    简介 Memcached 是一个高性能的分布式内存对象的key-value缓存系统,用于动态Web应用以减轻数据库负载,现在也有很多人将它作为内存式数据库在使用,memcached…

    springboot 2023年4月24日
    241
  • SpringBoot 缓存 – Redis

    引入依赖 缓存配置 启用缓存 修改项目启动类,增加注解@EnableCaching,开启缓存功能,如下: 配置缓存类 新建Redis缓存配置类RedisConfig,如下: 添加缓…

    2023年4月23日
    160
  • SpringBoot自定义注解与使用

    简介 注解是一种能添加到Java代码中的元数据,方法,类,参数与包都可以使用注解进行修饰,可以将注解看为一种特殊的标记,在Java编译或运行过程中将有这种特殊标记的部分进行特殊处理…

    springboot 2024年1月25日
    254
  • SpringBoot 整合Redis

    添加依赖 在pom.xml文件添加redis的依赖,如下: 配置 在application.yml文件中添加Redis相关的配置项,如下所示: 案例 1、字符串元素 2、List元…

    springboot 2023年3月26日
    121
  • Springboot 注解大全-@Import

    作用 @Import可以用来批量导入需要注册的各种类,如普通的类、配置类,完成普通类和配置类中所有bean注册到spring容器中。作用范围:作用于类、注解 定义 参数 value…

    springboot 2022年8月15日
    184