日志中心学习周报-2,SpringSecurity学习使用

简述SpringSecurity:

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

孩子们,图片是偷来的,B站up主三更讲的可以

图中只展示了核心过滤器,其它的非核心过滤器并没有在图中展示。

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

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

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

 具体的认证流程图:

具体实现:一般Web应用的需要进行认证授权

认证思路:

     一般我们会修改UsernamePasswordAuthenticationFliter和 InMemoryUserDetailsManager两个过滤器。
  SpringSecurity在默认的认证过程中如果账号密码校验成功会返回Authentication对象,之后UsernamePasswordAuthenticationFilter会将用户信息Authentication存入SecurityContextHolder中,但是我们在实际运用场景中认证通过后还需要向前端返回一个JSON格式的数据里面包括了JWT,所以此时我们需要写一个自定义登录接口来替代UsernamePasswordAuthenticationFliter的功能,用我们自己写的接口去调用调用ProviderManager的方法进行认证。
     InMemoryUserDetailsManager作用就是帮我们进行用户认证,我们可以重写UserDetailsService类中的方法去修改认证方式。

实现:

引入Maven依赖:
<!--        SpringSecurity-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

 

修改InMemoryUserDetailsManager认证方式(重写UserDetailsService类中的方法):

SpringSecurity 默认是在内存中查找对应的用户名密码然后UserDetailsService的默认实现类使用封装成UserDetail对象交给DaoAuthenticationProcider校验

但是我们在实际运用场景中是从数据库中查找用户信息

所以此时我们需要写一个UserDetailsService的实现类用来在数据库中查询用户信息并且封装到UserDetail对象中

并且需要写一个UserDetail的实现类因为用户信息不仅仅只有用户名和密码还有其他信息

package com.swp.springsecurity.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.swp.springsecurity.domain.LoginUser;
import com.swp.springsecurity.domain.User;
import com.swp.springsecurity.mapper.MenuMapper;
import com.swp.springsecurity.mapper.UserMapper;
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;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
    
    @Autowired
    UserMapper userMapper;
    @Autowired
    MenuMapper menuMapper;
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 根据用户名查询用户信息
        LambdaQueryWrapper wrapper = new LambdaQueryWrapper<User>().eq(User::getUserName, username);
        User user = userMapper.selectOne(wrapper);

        //如果没有该用户就抛出异常
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户名或密码错误");
        }

        // 查询权限信息封装到LoginUser中
        List<String> list = menuMapper.selectPermsByUserId(user.getId());

        // 将用户信息封装到UserDetails实现类中
        return new LoginUser(user,list);
    }
}
我们自定义的UserDetail实现类:
package com.swp.springsecurity.domain;

import com.alibaba.fastjson.annotation.JSONField;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;

@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {

    private User user;
    private List<String> permissions;
    @JSONField(serialize = false)
    private List<SimpleGrantedAuthority> authorities;

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

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(authorities != null)
            return authorities;
        List<SimpleGrantedAuthority> authorities = permissions.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
        return authorities;
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUserName();
    }

    //是否未过期
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    //是否未锁定
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    //凭证是否未过期
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    //是否可用
    @Override
    public boolean isEnabled() {
        return true;
    }
}

 

自定义登录登出接口,在接口中调用ProviderManager方法去认证
这里附上Service层代码:
package com.swp.springsecurity.service.impl;

import com.swp.springsecurity.domain.LoginUser;
import com.swp.springsecurity.domain.ResponseResult;
import com.swp.springsecurity.domain.User;
import com.swp.springsecurity.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
import com.swp.springsecurity.utils.JwtUtil;
import com.swp.springsecurity.utils.RedisCache;
import org.springframework.web.bind.annotation.RequestBody;

import java.util.HashMap;

@Service
public class LoginServiceImpl implements LoginService {
    @Autowired
    AuthenticationManager authenticationManager;
    @Autowired
    RedisCache redisCache;


    @Override
    public ResponseResult login(User user) {

        //1.封装Authentication对象
        UsernamePasswordAuthenticationToken authenticationToken =
                new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());

        //2.通过AuthenticationManager的authenticate方法来进行用户认证
        Authentication authenticated = authenticationManager.authenticate(authenticationToken);

        //3.在Authentication中获取用户信息
        LoginUser loginUser = (LoginUser) authenticated.getPrincipal();
        String userId = loginUser.getUser().getId().toString();
        //4.认证通过生成token
        String jwt = JwtUtil.createJWT(userId);
        //5.用户信息存入redis
        redisCache.setCacheObject("login:" + userId, loginUser);
        //6.把token返回给前端
        HashMap<Object, Object> hashMap = new HashMap<>();
        hashMap.put("token", jwt);
        return new ResponseResult(200, "登录成功", hashMap);
    }

    @Override
    public ResponseResult logout() {
        //获取SecurityContextHolder中的用户id
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUser loginUser = (LoginUser) authentication.getPrincipal();
        Long userId = loginUser.getUser().getId();
        //删除redis中的用户信息
        redisCache.deleteObject("login:" + userId);
        return new ResponseResult(200, "退出成功");
    }
}
注意要让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问,我们新建一个SpringSecurity的配置类:
package com.swp.springsecurity.config;

import com.swp.springsecurity.filter.JwtAuthenticationTokenFilter;
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.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.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
public class SecurityConfig{

    @Autowired
    JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Autowired
    AuthenticationConfiguration authenticationConfiguration;


    // 实际项目中我们不会把密码明文存储在数据库中。默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
        return authenticationConfiguration.getAuthenticationManager();
    }

    /**
     * 配置Spring Security的过滤链。
     *
     * @param http 用于构建安全配置的HttpSecurity对象。
     * @return 返回配置好的SecurityFilterChain对象。
     * @throws Exception 如果配置过程中发生错误,则抛出异常。
     */
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                // 禁用CSRF保护
                .csrf(AbstractHttpConfigurer::disable)
                // 设置会话创建策略为无状态
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                // 配置授权规则   指定user/login路径.允许匿名访问(未登录可访问已登陆不能访问). 其他路径需要身份认证
                .authorizeHttpRequests(auth -> auth.requestMatchers(new AntPathRequestMatcher("/user/login")).permitAll().anyRequest().authenticated())
                //开启跨域访问
                .cors()
                .and()
                .addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);

        // 构建并返回安全过滤链
        return http.build();
    }
}

 

授权思路:

      在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication,然后设置我们的资源所需要的权限即可。

实现:

启动类上开启SpringSecurity权限控制:
@EnableGlobalMethodSecurity(prePostEnabled = true)
在我们想要进行权限控制的类上加@PreAuthorize(“可以自定义返回值boolean的方法”),这里用的是SpringSecurity提供的方法hasAuthority():
// 注意字符串里面还有字符串要用单引号
 @PreAuthorize("hasAuthority('test')")
package com.swp.springsecurity.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {


    @RequestMapping("/hello")
    @PreAuthorize("hasAuthority('test')")
    public String hello(){
        return "World Hello";
    }

}
这里将对应的权限信息在UserDetailService环节就从数据库查询并存入了:

权限实际上就是一个个字符串:

// 查询权限信息封装到LoginUser中
List<String> list = menuMapper.selectPermsByUserId(user.getId());
package com.swp.springsecurity.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.swp.springsecurity.domain.LoginUser;
import com.swp.springsecurity.domain.User;
import com.swp.springsecurity.mapper.MenuMapper;
import com.swp.springsecurity.mapper.UserMapper;
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;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    UserMapper userMapper;
    @Autowired
    MenuMapper menuMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        // 根据用户名查询用户信息
        LambdaQueryWrapper wrapper = new LambdaQueryWrapper<User>().eq(User::getUserName, username);
        User user = userMapper.selectOne(wrapper);

        //如果没有该用户就抛出异常
        if (Objects.isNull(user)) {
            throw new RuntimeException("用户名或密码错误");
        }

        // 查询权限信息封装到LoginUser中
        List<String> list = menuMapper.selectPermsByUserId(user.getId());

        // 将用户信息封装到UserDetails实现类中
        return new LoginUser(user,list);
    }
}

        上述就是SpringSecurity的基本使用了,具体的可以查看B站三更老师对应网课,附上大佬的详细代码实现:README.md · Liu332256/SpringSecurity从入门到精通 - Gitee.com

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值