1. SpringSecurity概述
1.1 权限框架
目前市面上比较流行的权限框架主要实Shiro和Spring Security,这两个框架各自侧重点不同,各有各的优劣。
1.1.1 Apache Shiro
Apache Shiro(读作"sheeroh”,即日语"城")是一个开源安全框架,提供身份验证、授权、密码学和会话管理。Shiro框架直观、易用,同时也能提供健壮的安全性。
特点:
Shiro的特点:
1. 易于理解的Java Security APl;
2. 简单的身份认证(登录),支持多种数据源(LDAP,JDBC,Kerberos,ActiveDirectory等);·
3. 对角色的简单的签权(访问控制),支持细粒度的签权;
4. 支持一级缓存,以提升应用程序的性能;
5. 内置的基于POJO企业会话管理,适用于Web 以及非 Web的环境;
6.异构客户端会话访问;
7.非常简单的加密API;
8.不跟任何的框架或者容器捆绑,可以独立运行。
1.1.2 SpringSecurity
Spring Security是一个能够为基于Spring的企业应用系统提供描述性安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC(依赖注入,也称控制反转)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
Spring Security是Spring家族中的一个安全管理框架。相比与另外一个安全框架Shiro,它提供了更丰富的功能,社区资源也比shiro丰富。一般Web应用的需要进行认证和授权。而认证和授权也是SpringSecurity作为安全框架的核心功能。
SpringSecurity的特点:
1. 与Spring Boot集成非常简单。
2. 功能强大,高度可定制化。
3. 支持OAuth2.0。
4. 强大的加密ARI。
5. 防止跨站请求伪造攻击(CSRF)。
6. 提供Spring Cloud分布式组件。
1.1.3 权限框架的选择
Spring Security 是 Spring 家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。
相对于 Shiro,在 SSM 中整合 Spring Security 都是比较麻烦的操作,所以,SpringSecurity 虽然功能比 Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 Spring Security。
1.2 授权和认证
一般来说,Web应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是SpringSecurity重要核心功能。
(1)用户认证:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
(2)用户授权:是验证某个用户是否有权限执行某个操作。经过认证后判断当前用户是否有权限进行某个操作。
下面进行介绍一下RBAC:
RBAC (Role Based Access Control)基于角色的访问控制,通过抽象出“用户、角色、权限”"三个概念,实现用户分配角色,角色分配权限的权限管理方式,也是目前企业中权限管理主要实现方案。
案例如下图所示:
1.3 SpringSecurity的功能
Spring Security对Web安全性的支持大量地依赖于Servlet过滤器。这些过滤器拦截进入请求,并且在应用程序处理该请求之前进行某些安全处理。 Spring Security提供有若干个过滤器,它们能够拦截Servlet请求,并将这些请求转给认证和访问决策管理器处理,从而增强安全性。
如今的Spring Security已经成为Spring Framework下最成熟的安全系统,它为我们提供了强大而灵活的企业级安全服务,如:
Ø 认证授权机制
Ø Web资源访问控制
Ø 业务方法调用访问控制
Ø 领域对象访问控制Access Control List(ACL)
Ø 单点登录(Central Authentication Service)
Ø 信道安全(Channel Security)管理等功能
2.SpringSecurity 实战
2.1 引入SpringSecurity
导入依赖
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
引入依赖后我们在尝试去访问之前的接口就会自动跳转到一个SpringSecurity的默认登陆页面,默认用户名是user,密码会输出在控制台。必须登陆之后才能对接口进行访问。
2.2 认证
2.2.1 登录校验流程
登录校验流程如下图所示:
2.2.2 SpringSecurity完整流程
SpringSecurity的原理其实就是一个过滤器链,内部包含了提供各种功能的过滤器。这里我们可以看看入门案例中的过滤器。
UsernamePasswordAuthenticationFilter:负责处理我们在登陆页面填写了用户名密码后的登陆请求。入门案例的认证工作主要有它负责。
ExceptionTranslationFilter:处理过滤器链中抛出的任何AccessDeniedException和AuthenticationException。
FilterSecurityInterceptor:负责权限校验的过滤器。
我们可以通过Debug查看当前系统中SpringSecurity过滤器链中有哪些过滤器及它们的顺序。如下图所示:
2.2.3 认证流程详解
Authentication接口:它的实现类,表示当前访问系统的用户,封装了用户相关信息。
AuthenticationManager接口:定义了认证Authentication的方法。
UserDetailsService接口:加载用户特定数据的核心接口。里面定义了一个根据用户名查询用户信息的方法。
UserDetails接口:提供核心用户信息。通过UserDetailsService根据用户名获取处理的用户信息要封装成UserDetails对象返回。然后将这些信息封装到Authentication对象中。
2.3 思路分析
在前后端分离中我们一般采用token验证,所以在这里我们要自定义登录接口,通过自定义接口调用调用ProviderManager的方法进行认证 ,如果认证通过生成jwt,然后将jwt返回给前端,同时将用户信息包括用户权限信息等存入到redis数据库中,redis数据库作为缓存读取速度远远大于从数据库中进行读取用户信息。我们还要自定义UserDetailsService,在这个实现类中去查询数据库。除此之外我们还需要定义JWT认证过滤器,从前端请求头中获取token,解析token获取其中的userid,然后根据userid从redis中获取用户信息存入SecurityContextHolder。具体流程如下图:
2.4 代码实战
2.4.1 自定义UserDetailsService类
首先定义一个UserDetails类代码如下:
@Data @AllArgsConstructor @NoArgsConstructor public class LoginUser implements UserDetails, Serializable { private User user; //返回权限信息 @Override public Collection<? extends GrantedAuthority> getAuthorities() { return null; } //返回用户密码 @Override public String getPassword() { return user.getPassword(); } //返回用户账号 @Override public String getUsername() { return user.getLoginname(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } }
自定义实现UserDetailsService接口,重写其中的方法。代码如下:
@Service public class UserDetailServiceImpl implements UserDetailsService { @Autowired TbUserMapper tbUserMapper; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { //根据用户名从数据库中查询数据信息 QueryWrapper<TbUser> queryWrapper=new QueryWrapper<>(); queryWrapper.eq("username",username); TbUser tbUser = tbUserMapper.selectOne(queryWrapper); //查询不到该用户信息抛异常 if(tbUser==null){ throw new RuntimeException("用户名或者密码错误"); } //封装成UserDetails返回 return new MyUser(tbUser); } }
2.4.2 密码加密存储
实际项目中我们不会把密码明文存储在数据库中。
1. 默认使用的PasswordEncoder要求数据库中的密码格式为:{id}password 。它会根据id去判断密码的加密方式。但是我们一般不会采用这种方式。所以就需要替换PasswordEncoder。
2.我们一般使用SpringSecurity为我们提供的BCryptPasswordEncoder。
3.我们只需要使用把BCryptPasswordEncoder对象注入Spring容器中,SpringSecurity就会使用该PasswordEncoder来进行密码校验。
4.我们可以定义一个SpringSecurity的配置类,SpringSecurity要求这个配置类要继承WebSecurityConfigurerAdapter。
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder(){ return new BCryptPasswordEncoder(); } }
2.4.3 登录接口
我们需要自定义登陆接口,然后让SpringSecurity对这个接口放行,让用户访问这个接口的时候不用登录也能访问。
在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置把AuthenticationManager注入容器。
认证成功的话要生成一个jwt,放入响应中返回。并且为了让用户下回请求时能通过jwt识别出具体的是哪个用户,我们需要把用户信息存入redis有效时间为30分钟,可以把用户id作为key。登录接口代码如下:
Configuration代码如下:
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Bean PasswordEncoder password(){ return new BCryptPasswordEncoder(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable()//关闭csrf .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) //不通过session获取SecurityContext .and() .authorizeRequests() .antMatchers("/user/login").anonymous() //对于登录接口允许匿名访问 .anyRequest().authenticated(); //除上面外的所有请求全部需要鉴权认证 } }
登录接口代码如下:
@Autowired RedisTemplate redisTemplate; @Autowired AuthenticationManager authenticationManager; @PostMapping("/user/login") public Result login(@RequestBody TbUser tbUser){ //通过AuthenticationManager的authenticate方法来进行用户认证 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(tbUser.getUsername(),tbUser.getPassword()); Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken); if(authenticate==null){ return Result.error("401","登录校验失败"); } else { //获取用户信息 MyUser myUser = (MyUser) authenticate.getPrincipal(); //获取用户id Long id = myUser.getTbUser().getId(); //根据用户id生成token String token = JwtUtil.generateToken(id); //将token放在数据库中 redisTemplate.opsForValue().set(String.valueOf(id),myUser,30, TimeUnit.MINUTES); return Result.success(token); } }
2.4.4 token认证过滤器
需要自定义一个过滤器,这个过滤器会去获取请求头中的token,对token进行解析取出其中的userid。使用userid去redis中获取对应的MyUser对象。然后封装Authentication对象存入SecurityContextHolder。代码如下:
JwtAuthenticationTokenFilter自定义认证过滤器代码如下:
@Component public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { @Autowired RedisTemplate redisTemplate; @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { //获取请求头中的token String token = httpServletRequest.getHeader("token"); //如果token为空直接放行,由于用户信息没有存放在SecurityContextHolder.getContext()中所以后面的过滤器依旧认证失败符合要求 if(!StringUtils.hasText(token)){ filterChain.doFilter(httpServletRequest,httpServletResponse); return; } Long userId; try { //通过jwt工具类解析token获得userId,如果token过期或非法就会抛异常 DecodedJWT decodedJWT = JwtUtil.decodeToken(token); userId = decodedJWT.getClaim("userId").asLong(); }catch (Exception e){ e.printStackTrace(); throw new RuntimeException("token非法"); } //根据userId从redis中获取用户信息,如果没有该用户就代表该用户没有登录过 MyUser myUser = (MyUser) redisTemplate.opsForValue().get(String.valueOf(userId)); if(Objects.isNull(myUser)){ throw new RuntimeException("用户未登录"); } //将用户信息存放在SecurityContextHolder.getContext(),后面的过滤器就可以获得用户信息了。 UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(myUser,null,null); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); filterChain.doFilter(httpServletRequest,httpServletResponse); } }
在Configuration类中配置自定义过滤器,代码如下:
@Configuration public class SecurityConfiguration extends WebSecurityConfigurerAdapter { @Autowired private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter; //自定义过滤器放在UsernamePasswordAuthenticationFilter过滤器之前 http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } }
2.4.5 接口测试
在访问/user/login接口之前,其余的接口都不能访问到,然后先登录访问/user/login接口,测试结果如下图:
然后携带请求头token访问其它方法就可以正常的进行访问了,由此可见我们的代码测试验证成功。
至此该篇文章SpringSecurity认证内容结束,下一篇文章作者将继续写SpringSecurity授权内容,未完待续。