Spring Security 重点解析(上)

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: Spring Security 重点解析

1. 简介

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

  • 一般来说中大型的项目都是使用SpringSecurity 来做安全框架。小项目有Shiro的比较多,因为相比与SpringSecurity,Shiro的上手更加的简单。
  • 一般Web应用的需要进行认证和授权。

认证:

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

授权:

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

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

接下来以用户名密码登录这个案例来讲解!

Spring Security 推荐学习视频:SpringSecurity框架教程-Spring Security+JWT实现项目级前端分离认证授权-挑战黑马&尚硅谷_哔哩哔哩_bilibili

2. 依赖

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

引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个 SpringSecurity的默认登陆页面

  • 默认用户名是user,密码会输出在控制台。

  • 必须登陆之后才能对接口进行访问。

退出登录:

等一下你就知道原理了~

3. 登录认证

3.1 登录校验流程

3.2 Spring Security 默认登录的原理

3.2.1 Spring Security 完整流程

SpringSecurity 的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。

  • 经过所有过滤器才能访问 API,过滤器也能处理响应啥的~

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

  1. UsernamePasswordAuthenticationFilter
  • 负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责
  1. ExceptionTranslationFilter
  • 处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException ,有异常抛出就肯定过不了这个过滤器了
  1. FilterSecurityInterceptor
  • 负责权限校验的过滤器。
3.2.2 登录逻辑探究

UsernamePasswordAuthenticationFilter 就是默认登录方式,默认是 Cookie-Session 机制

概念速查:

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

AuthenticationManager接口:定义了认证Authentication的方法

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

UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装

成UserDetails对象返回。然后将这些信息封装到Authentication对象中。

  1. 访问 /test/hello 第一次没有会话,UsernamePasswordAuthenticationFilter 尝试获取,获取不到,强制让用户登录(重定向到[GET] /login 页面,本次请求不算数)
  2. 访问 [POST]/login 携带用户名密码,验证成功后,将 “用户相关信息(UserDetails)”构造出一个核心对象(Authentication),放入这次请求的 Security 框架上下文对象中,前往之后的拦截器

  1. 其他过滤器都没问题了(登录没啥权限要求),请求成功到达[POST]/login ,进行对应逻辑,返回响应~

  1. 访问 /test/hello 携带 Cookie,UsernamePasswordAuthenticationFilter 获取到会话记录 UserDetails,”构造出一个核心对象(Authentication),放入这次请求的 Security 框架上下文对象中,前往之后的拦截器

会话记录:

  1. 其他过滤器都没问题了(登录没啥权限要求),请求成功到达/test/hello ,进行对应逻辑,返回响应~

3.3 自定义改动

  1. 自定义用户名密码校验方式
  2. 自定义拦截器,可调节在哪个拦截器之前或之后,或者删除哪些拦截器
3.3.1 自定义用户密码校验
  1. 自定义的 UserDetails 获取方式 F1
  2. 自定义密码的加密算法 F2

[POST]/login 携带的用户名为 U,密码为 P,UserDetails 为 D

  • D = F1(U);
  • 判断 F2(P)D.getPassword(),是否相同
3.3.2 自定义 UserDetails 获取方式 F1

我知道我没有给出一些数据库与MP相关的代码,不过思路看得懂即可!

@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser implements UserDetails {
    private SysUser user;
    private List<String> permissions;
    @JSONField(serialize = false)//代表,由不通过fastjson序列化,用系统redis其他的方式存入redis
    private List<GrantedAuthority> authorities;//com.alibaba.fastjson.JSONException: autoType is not support.
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        if(Objects.isNull(this.authorities)) {
            // 将 permissions 的权限信息封装成 SimpleGrantedAuthority 象,并且也是集合
            this.authorities = this.permissions.stream()
                    .parallel()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
        }
        return this.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;
    }
    public LoginUser(SysUser user, List<String> permissions) {
        this.user = user;
        this.permissions = permissions;
    }
}
@Service
@Slf4j
public class UserDetailsServiceImpl implements UserDetailsService {
    @Resource
    private SysMenuService menuService;
    // 查询用户信息
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        // 查询用户信息
        SysUser user = Db.lambdaQuery(SysUser.class)
                .eq(SysUser::getUserName, s)
                .one();
        if(Objects.isNull(user)) {
            log.warn("用户名或者密码错误");
            throw new RuntimeException("用户名或者密码错误");
        }
        // todo 查询对应的权限信息
        List<String> permissions = menuService.selectMenuById(user.getId());
        // 把数据封装成UserDetails返回
        return new LoginUser(user, permissions);
    }
}

认证的时候,就会用我们实现的加载数据的方法

3.3.3 自定义加密算法 F2
//BCryptPasswordEncoder的Bean对象加入到容器里
@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

在认证的时候,就是用我们的这个 PasswordEncoder 去加密(默认不加密,不用加密算法)

用同种加密算法去注册两个账号:

@Resource
    private PasswordEncoder encoder;
    @Test
    public void createUser() {
        List<SysUser> list = new ArrayList<SysUser>() {{
            add(new SysUser(){{
                setId(1L);
                setNickName("马1号");
                setUserName("mara1");
                setPassword(encoder.encode("123456"));
            }});
            add(new SysUser(){{
                setId(2L);
                setNickName("马2号");
                setUserName("mara2");
                setPassword(encoder.encode("123456"));
            }});
        }};
        Db.saveBatch(list);
    }

3.4 自定义登录认证

对于以上内容,依赖默认的登录页面,获取是用户名密码的机制也比较单一,我们往往需要一些其他的登录方式~

接下来,我们来搞一下自定义登录(还是以用户名密码,但是这次是我们自己写的业务逻辑)

3.4.1 自定义登录接口

@Data
public class FormUserDTO {
    private String userName;
    private String password;
    
}
@RestController
//@RequestMapping("/user")
public class LoginController {
    @Resource
    private LoginService loginService;
    @PostMapping("/user/login")
    public ResponseResult login(@RequestBody FormUserDTO user) {
        //登录
        return loginService.login(user);
    }
}
public interface LoginService {
    ResponseResult login(FormUserDTO user);
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

实现:

@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisCache redisCache;
@Override
public ResponseResult<Map<String, String>> login(FormUserDTO user) {
    // 获取AuthenticationManager authenticate认证
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword());
    Authentication authenticate = authenticationManager.authenticate(authenticationToken);
    //如果认证不通过,提示
    if(Objects.isNull(authenticate)) {
        log.warn("登录失败");
        throw new RuntimeException("登录失败");
    }
    //如果认证通过了,用userid生成一个jwt
    LoginUser loginUser = (LoginUser) authenticate.getPrincipal();//认证的时候已经调用了getAuthorities了
    String userid = loginUser.getUser().getId().toString();
    String jwt = JwtUtil.createJWT(userid);
    //把完整的用户信息存入redis,userid作为key
    redisCache.setCacheObject("login:" + userid, loginUser);
    //        redisCache.getCacheObject("login:" + userid);//是完完整整的存进去的
    // 符合jwt
    return new ResponseResult(200, "登录成功", new HashMap<String, String>(){{
        this.put("token", jwt);
    }});
}

这就相当于替 UsernamePasswordAuthenticationFilter 调用了认证方法获得 authenticate:

自然就是一轮认证:

认证失败也是在这个过程中抛出异常的

3.4.2 自定义过滤器
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {// 保证请求只会经过这个过滤器一次
    
    @Resource
    private RedisCache redisCache;
    
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        // 获取token
        String token = httpServletRequest.getHeader("token");
        if(!StringUtils.hasText(token)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
            return;
        }
        // 解析token
        String userid = null;
        try {
            Claims claims = JwtUtil.parseJWT(token);
            userid = claims.getSubject();
        } catch (Exception e) {
            log.error("token非法");
            filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
            return;
        }
        //从redis中获取用户信息
        String redisKey = "login:" + userid;
        LoginUser loginUser = redisCache.getCacheObject(redisKey);
        if(Objects.isNull(loginUser)) {
            filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
            return;
        }
        // 存入SecurityContextHolder
        UsernamePasswordAuthenticationToken authenticationToken
                = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());//代表已认证
        SecurityContextHolder.getContext().setAuthentication(authenticationToken);
        filterChain.doFilter(httpServletRequest, httpServletResponse);//放行
    }
}

认证成功要记录!

3.4.3 Security 配置

yaml 配置文件去配置不灵活,不鲜明,在这里不演示

@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)//开启权限控制,不开启这个,注解的权限控制不能生效
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Resource
    private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
    @Resource
    private AuthenticationEntryPoint authenticationEntryPoint;
    @Resource
    private AccessDeniedHandler accessDeniedHandler;
    //BCryptPasswordEncoder的Bean对象加入到容器里
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    @Override
    protected void configure(HttpSecurity http) throws Exception {
//        super.configure(http); 默认是cookie session
    //内部指定表单,用户输入表单就去验证,也就是UsernamePasswordAuthenticationFilter的认证方式
    // 既然注释掉了,则代表这个过滤器(可能不止这一个)失效了~(不被调用 -> 过滤器不工作),并且还注释掉“控制”相关的配置
        http
    //关闭csrf
            .csrf().disable()
    //不通过Session获取SecurityContext
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/user/login") /*匿名访问*/.anonymous()
        // 添加过滤器
        http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);//添加过滤器在某个过滤器之前
    }
}

不必调用父类的configure方法,UsernamePasswordAuthenticationFilter就相当于没用了

UsernamePasswordAuthenticationFilter 无法进行重定向

不请求`[POST]/login` ,UsernamePasswordAuthenticationFilter就不会调用认证的方法,SecurityContextHolder就不会有对应的那个Authentication对象的记录
最终需要认证的接口就访问不了!
  • “哪些可以匿名访问不用认证,哪些需要进行认证”,只不过是一些“认证/权限等等的控制”罢了,没有实际的认证机制
  • 每个请求都会经过过滤器链,只不过一些没通过检查也无所谓!

这样我们就需要自己写个拦截器,去规定怎么样才算认证成功

这样,必须提前在/user/login (接口内用了认证方法,这一步代表体检做认证),将userid存到redis,生成token返回
请求必须携带token,由 jwtAuthenticationTokenFilter 进行认证是否有token,redis是否有记录
有则代表提前做了认证,SecurityContextHolder 设置那个Authentication对象,放行即可,就不需要进行UsernamePasswordAuthenticationFilter的那个默认login页面去认证了


把默认的关了的话,UsernamePasswordAuthenticationFilter 就相当于没用了,UsernamePasswordAuthenticationFilter 并不会让认证结果有任何变化

没有 jwtAuthenticationTokenFilter 的话,那么 SecurityContextHolder 就没有记录,那么请求必然不通过认证/验权

jwtAuthenticationTokenFilter 没有让 SecurityContextHolder 有记录,那么请求也必然不通过认证/验权

3.4.4 退出登录
@GetMapping("/user/logout")
public ResponseResult logout() {
    return loginService.logout();
}
public interface LoginService {
    ResponseResult login(FormUserDTO user);
    ResponseResult logout();
}

实现:

@Override
public ResponseResult logout() {
    //获取用户id
    UsernamePasswordAuthenticationToken authentication
            = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    LoginUser loginUser = (LoginUser) authentication.getPrincipal();
    String userid = loginUser.getUser().getId().toString();
    // 删除redis中的值
    String redisKey = "login:" + userid;
    redisCache.deleteObject(redisKey);
    return new ResponseResult(200,  "注销成功");
}

补充,用户名为 null,在 loadUserByUsername 方法的 userName 为"NONE_PROVIDED"

  • 如果真的有用户名是这个,密码还没错,还真的能登录成功!

这是借用了 UsernamePasswordAuthenticationFilter 的认证方法,我们当然也可以自己写一个认证方法,构造UserDetails 啊

  • 也可以研究其他的 SpringSecurity 提供的认证方法~

Spring Security 重点解析(下):https://developer.aliyun.com/article/1509659

目录
相关文章
|
18天前
|
XML 安全 前端开发
Spring Security 重点解析(下)
Spring Security 重点解析
37 1
|
18天前
|
缓存 前端开发 Java
【框架】Spring 框架重点解析
【框架】Spring 框架重点解析
32 0
|
18天前
|
canal 缓存 关系型数据库
Spring Boot整合canal实现数据一致性解决方案解析-部署+实战
Spring Boot整合canal实现数据一致性解决方案解析-部署+实战
|
7月前
|
缓存 安全 算法
Spring Security OAuth 2.0 资源服务器— JWT
Spring Security OAuth 2.0 资源服务器— JWT
294 1
|
18天前
|
安全 Java 数据安全/隐私保护
Spring Security OAuth 之 @EnableAuthorizationServer 干了啥?
【1月更文挑战第19天】在引入 Spring Security OAuth 的 starter 后,可以方便地使用注解,自动开启和配置授权服务组件。它是如何完成这些配置的?
158 2
|
18天前
|
安全 NoSQL Java
Spring Security OAuth 令牌生成
【1月更文挑战第17天】之前写了两篇分析 Spring Security OAuth 认证流程的文章,这篇主要来分析一下,`tokenServices.createAccessToken` 方法,具体是怎么生成 Token。
39 2
|
JSON 安全 Java
Spring Security OAuth 实现 GitHub 快捷登录
Spring Security 5中集成了OAuth的客户端模块,该模块包含以下三个子模块
Spring Security OAuth 实现 GitHub 快捷登录
|
安全 前端开发 Java
从一手资料学习--Spring Security与OAuth(二)
上回我们聊到,既然Spring官网也有提到,要学习Spring Security OAuth相关的知识,最好先学习OAuth2.0相关的知识,而官网中OAuth 2.0 Framework的链接地址对应的就是rfc6749的文档,结构是这样的
从一手资料学习--Spring Security与OAuth(二)
|
安全 Java 开发者
从一手资料学习--Spring Security与OAuth(一)
不知道大家对于上面的几个问题被问及的时候会心里发慌。强哥发现,大多数小伙伴对于一些工作中使用较少的知识,或者说是平常都在用,但是不需要自己去实现的知识,主动学习的积极性都比较低。
从一手资料学习--Spring Security与OAuth(一)
|
JSON 安全 Java
再见,Spring Security OAuth!!
本次将 《Spring Authorization Server》项目正式上线,去掉了之前的体验状态,此举恰逢 0.2.0 版本发布,这也是第一个正式支持的生产就绪版本。
再见,Spring Security OAuth!!

推荐镜像

更多