SpringSecurity

资料来自:b站up主:  三更草堂

        学习是为了配合up主的springboot项目,里面用到了这个技术,所以配合学习;需要资料的可以去找up要;

        自己就是为了记录一下,省略了准备工作,方便回忆(不然滑过准备工作有点累~)

目录

资料来自:b站up主:  三更草堂

一、什么是SpringSecurity

二、快速入门

三、认证

3.1 认证流程

3.2 代码实现

 3.3 密码加密

四、登录

4.1 逻辑分析

4.2 代码实现

五、退出

5.1 逻辑分析

5.2 代码实现

六、授权

6.1 逻辑分析

6.2 开启权限

6.3 基础版:权限写死


一、什么是SpringSecurity

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

一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。

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

认证:验证当前访问系统的是不是本系统的用户,并且要确认具体是哪个用户

授权:经过认证后判断当前用户是否有权限进行某个操作

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

二、快速入门

springBoot整合SpringSecurity非常容易,只需要导入相应的依赖

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

 导入后启动项目发送请求,首先进入默认的登录页:

 默认用户名user, 在启动的时候会在控制台显示密码,这也就下面认证中默认从内存中进行验证的原因。

登录校验流程

三、认证

3.1 认证流程

        从上图中可以分析到:用户提交数据后首先经过UserNamePasswordxxxFilter进行身份验证,经过两个类间的函数调用到:ProviderManger, DaoxxxProvider

        在UserDeatilsService接口实现类中的 loadUserByUsername, 参数为用户输入的username, UserDeatilsService的默认实现类在内存中取得数据,然后封装成UserDetails对象进行返回

从上面的流程中有两个问题怎么通过jwt生成token, 不能从内存中取得数据

因此如果想要定制security, 可以在UserNamePasswordxxxFilter, 以及UserDeatilsService 自己实现从数据库中获取数据

3.2 代码实现

UserDetailsServiceImpl
/**
 *  在用户登录的逻辑中,用户提交信息后,会经过UserName的过滤器,经过方法调用走到默认的UserDetailsService
 *  的实现类中,在内存中获取用户的信息然后封装成UserDetails对象;
 *  所以自己重写UserDetailsService实现类,这样在调用的时候就会走自定义实现,然后从数据库中获取数据,封装后返回给调用者
 *
 *  UserDetails是一个接口,需要自定义实现类
 */
@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private UserMapper userMapper;

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

        // 根据username从数据库中获取数据
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.eq(User::getUserName, username);
        User user = userMapper.selectOne(queryWrapper);

        /*
            封装UserDetails类型对象, LoginUserDetails是自定义的具体实现类
            UserDeatils中的getPassword, getUserName是自动调用
         */
        UserDetails userDetails = new LoginUserDetails(user);

        return userDetails;
    }
}
LoginUserDetails
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUserDetails implements UserDetails {

    private User user;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return null;
    }

    @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;
    }
}

UserDetails是接口,所以需要具体实现类,目前需要将后面的boolean类型的方法改为true, 不然无法进行登录

此时需要在数据库中密码字段前面添加:{noop}, 表示密码是明文,默认有PasswordEncoder校验

 3.3 密码加密

        前面需要在数据库密码字段前面添加{noop},是因为默认有PasswordEncoder对返回的UserDetails类型对象的数据和用户输入进行比较而默认的PasswordEncoder要求在密码字段前加上{id}进行核实,很不方便,所以引入BCryptPasswordEncoder进行替换

配置类:

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // Bean用来将对象注入容器
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }

}

 主要的方法:encode用来加密,matches用来暗文匹配

因此在注册用户时候,由于BCryptPasswordEncoder是注入容器的,所以使用@Autowired获取对象然后调用encode进行加密

测试:

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Test
    public void testPasswordEncoder() {
        String s1 = "123456";
        String s2 = "123456";
        String encode = bCryptPasswordEncoder.encode(s1);
        String encode1 = bCryptPasswordEncoder.encode(s2);
        System.out.println(encode);
        System.out.println(encode1);

    }

四、登录

4.1 逻辑分析

  • 首先需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。即 login请求不应该被拦截
  • 自定义登录接口中封装Authentication对象然后跳转ProviderManager完成后续调用
  • 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
  • 认证成功的话要生成一个jwt,放入响应中返回。
  • 为了让用户下回请求时能通过jwt识别出具体userid,进而获取用户的其他信息(同时减少数据库的频繁访问),我们需要把用户信息存入redis,可以把用户id作为key。

4.2 代码实现

自定义登陆接口

@RestController
public class LoginController {

    @Autowired
    private LoginService loginService;

    // 自定义登录控制器,在业务逻辑层中调用ProviderManager,模拟默认的实现流程
    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody User user) {
        return loginService.login(user);
    }

}

让SpringSecurity对这个接口放行

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 配置用于放行login页面
    @Override
    protected void configure(HttpSecurity http) throws Exception {

                //关闭csrf
        http.csrf().disable()
                //不通过Session获取SecurityContext
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .authorizeRequests()
                // 对于登录接口 允许匿名访问
                .antMatchers("/user/login").anonymous()
                // 除上面外的所有请求全部需要鉴权认证
                .anyRequest().authenticated();
    }
}

把AuthenticationManager注入容器。

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 代码省略

    // 将AuthenticationManager作为对象注入容器
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}

AuthenticationManager的authenticate方法来进行用户认证

/*
   将用户信息封装Authentication对象,然后调用authenticate方法
   到ProviderManager,由他完成后去的调用工作

   UsernamePasswordAuthenticationToken: Authentication的实现类
*/
Authentication authentication = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
Authentication authenticate = authenticationManager.authenticate(authentication);

从返回的验证对象中获取userid

 // 如果成功将userid使用jwt生成token返回前端
    // principal封装了验证后的用户信息
LoginUserDetails loginUser = (LoginUserDetails) authenticate.getPrincipal();
String userId = loginUser.getUser().getId().toString();

String jwt = JwtUtil.createJWT(userId);

用户信息存入redis

// 将user信息存入redis中
redisCache.setCacheObject("login:"+userId,loginUser);

认证成功的话要生成一个jwt,放入响应中返回。

Map<String, String> map = new HashMap<>();
map.put("token", jwt);
return new ResponseResult(200, "验证成功", map);

LoginService业务逻辑层的完整代码:

/**
 *  登录验证的业务逻辑层
 *
 *  默认的流程:在经过过滤器后,会将用户的信息封装成:Authentication
 *  然后调用ProviderManager等最后到UserDetailsService,在从数据库
 *  获取数据后封装成UserDetails, 使用PasswordEncoder验证后返回
 *  Authentication对象,如果非空,表示验证成功
 */
@Service
public class LoginServiceImpl implements LoginService {

    /**
     * 使用redis的原因:
     *  redis是菲关系型数据库,一般作为缓存的存在;
     *  在经过验证后,返回给前端token, 在下一次前端发送的请求中会携带
     *  token数据,传统情况,在将token解析后需要使用userid查询数据库;
     *  那么多次请求的数据库访问太频繁,所以使用redis作为中间缓存的对象。
     */

    @Autowired
    private AuthenticationManager authenticationManager;

    // RedisCache 工具类上面使用了@Component注入了容器
    @Autowired
    private RedisCache redisCache;


    @Override
    public ResponseResult login(User user) {

        /*
            将用户信息封装Authentication对象,然后调用authenticate方法
            到ProviderManager,由他完成后去的调用工作

            UsernamePasswordAuthenticationToken: Authentication的实现类
        */
        Authentication authentication = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
        Authentication authenticate = authenticationManager.authenticate(authentication);

        // 是否验证成功
        if (Objects.isNull(authenticate)) {
            throw new RuntimeException("验证错误");
        }

        // 如果成功将userid使用jwt生成token返回前端
            // principal封装了验证后的用户信息
        LoginUserDetails loginUser = (LoginUserDetails) authenticate.getPrincipal();
        String userId = loginUser.getUser().getId().toString();

        String jwt = JwtUtil.createJWT(userId);

        // 将user信息存入redis中
        redisCache.setCacheObject("login:"+userId,loginUser);

        // token响应给前端
        Map<String, String> map = new HashMap<>();
        map.put("token", jwt);
        return new ResponseResult(200, "验证成功", map);
    }
}

测试:

五、退出

5.1 逻辑分析

        前提:用户已经登录,在redis中可以获取用户信息

        实现: 前面定义了 JwtAuthenticationTokenFilter 过滤器,发送logout请求经过过滤器后将当前用户信息存储在 SecurityContextHolder 中;logout的业务逻辑层从 SecurityContextHolder中获取用户信息,得到userid在redis中进行删除

5.2 代码实现

@Service
public class LoginServiceImpl implements LoginService {

    /*
        登录后,首先通过JwtFilter认证将信息存储在SecurityContextHolder中,同类请求中的
        SecurityContextHolder是相同的,所以可以获取用户的id
     */
    @Override
    public ResponseResult logout() {

        // 从SecurityContextHolder获取userid
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        LoginUserDetails loginUser = (LoginUserDetails) authentication.getPrincipal();
            // 一定可以获取user信息,因为首先经过jwtFilter, 如果没有user会直接抛出异常
        Long userid = loginUser.getUser().getId();

        // 从redis中删除信息
        redisCache.deleteObject("login:" + userid);

        return new ResponseResult(200, "退出登录");
    }
}

六、授权

权限的作用:不同的用户可以使用不同的功能

6.1 逻辑分析

        在之前代码实现中预留的 TODO, 表示授权没有实现;

        在UserDeatilsServiceImpl从数据库中获取用户名, 密码以及权限信息;封装信息在LoginUserDetals中。

        在JwtAuthenticationTokenFilter过滤时,UsernamePasswordAuthenticationToken(user, null, null); 第三个参数就是权限列表;然后在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

6.2 开启权限

配置类:

@Configuration
// 开启授权
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter

请求方法:

@RestController
public class MyController {

    // 赋予权限
    @PreAuthorize("hasAuthority('test123')")
    @RequestMapping("/hello")
    public String sayHello() {
        return "hello";
    }
}

6.3 基础版:权限写死

UserDetailsServiceImpl 

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

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

        // 省略

        // TODO: 查询用户的权限信息
        List<String> perssions = new ArrayList<>(Arrays.asList("test", "admin"));

      
        UserDetails userDetails = new LoginUserDetails(user, perssions);

        return userDetails;
    }
}

LoginUserDetails

@Data
@NoArgsConstructor
public class LoginUserDetails implements UserDetails {

    private User user;

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

    // 记录转换后的权限信息;但是 GrantedAuthority 在reids中不能被序列化,所以需要忽略
    @JSONField(serialize = false)
    private List<GrantedAuthority> authorities;

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

    // 在过滤器链中会调用这个方法返回权限信息
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {

        // authorities信息如果不进行存储,每次都会重新封装,可以使用成员变量进行存储
        // 权限信息一定在用户登录的时候已经获取了
        if (!Objects.isNull(authorities)) {
            return authorities;
        }

        // 将perssions转换成GrantedAuthority类型的集合
        List<SimpleGrantedAuthority> authorities = permissions.stream()
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        return authorities;
    }

   // 省略
}
JwtAuthenticationTokenFilter

        修改下面方法中的第三个参数

 // TODO:用户的认证
    // 认证:获取权限后封装在LoginUserDetails中,可以调用getxx方法获取
Authentication authentication = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值