Spring Security 结合jwt 自定义权限认证 基于huike CRM系统

项目源码 前后端分离 前端在web包下

登录认证

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第1张图片

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第2张图片

验证码流程

请求 URL:

http://localhost:81/dev-api/captchaImage

找captchaImage

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第3张图片

先生成uuid 声明要返回的属性

String code验证码 
String capStr传给验证码生成器的验证码的公式
BufferedImage image 验证码生成器生成的图片

判断是math还是char类型的验证码

在成员属性定义 通过读取配置文件的方式

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第4张图片

 成员属性定义了两个Producer 分别通过@Resource注解装配

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第5张图片

 即为Producer接口多态的注入了两个不同的实现类captchaProducer和captchaProducerMath

 Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第6张图片

 captchaProducer 字符串型验证码:

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第7张图片

captchaProducerMath 数学运算型字符串:

这里就是生成验证码

 Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第8张图片

用redis缓存即将返回给前端的验证码信息

verifyKey作为redis缓存的key 
code即value  
Constants.CAPTCHA_EXPIRATION 是缓存的有效时间
TimeUnit.MINUTES  有效时间的时间类型

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第9张图片

最后将验证码信息写入流中 将流用AjaxResult封装 返回给前端AjaxResult对象

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第10张图片

jwt认证过滤器

这时候看SecurityConfig 继承 WebSecurityConfigurerAdapter

先要去SecurityConfig配置一个configure类 配置需要鉴权验证拦截和不需要验证放行的资源 需要对登录模块放行

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第11张图片

 查看SecurityConfig 可以继承重新的方法 重写authenticationManagerBean()

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第12张图片

用推荐的格式就行 如果只有一个authenticationManagerBean()不用起名字

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第13张图片

使用@Bean注入到ioc容器中

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第14张图片

这样就能在登录校验类SysLoginService中注入成员属性

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第15张图片

这样就可以调用它的认证方法来对账号密码校验

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第16张图片

 这个方法需要传一个authentication接口的实现类对象

可以找到这个接口所有的实现类对象

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第17张图片

用UsernamePasswordAuthenticationToken这个实现类 将用户名 密码封装进去 然后让authenticationManager进行认证操作

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第18张图片

这一步就是判断密码是否正确

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第19张图片

校验是在DaoAuthenticationProvider类中进行

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第20张图片

crm项目所使用的密码编码器加密

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第21张图片

 如果密码不匹配就扔出异常 接收处理后然后记录日志

authenticationManager最终会调用UserDetailsService的实现类UserDetailsServiceImpl中的重写的方法loadUserByUsername

 这里会去数据库找用户的数据 通过createLoginUser去找用户对应的permissions 权限列表 这里具体在后面授权实现分析

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第22张图片

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第23张图片

loadUserByUsername方法会根据username去数据库找用户数据 然后就可以根据返回结果判断登录用户状态

而异常会被捕获后进行判断 然后记录登录失败原因的日志 这里主要是用户状态的异常

如果没有异常 则说明登录成功 成功先记录日志 然后用authentication获取登录用户信息

将封装过的LoginUser对象传给TokenService层的createToken方法 来创建token

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第24张图片

先生成uuid字符串 封装至loginUser对象中

setUserAgent(loginUser);  设置用户loginUser的一些ip等信息

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第25张图片

refreshToken(loginUser);用redis根据前面生成的uuid缓存用户数据loginUser的信息

 

设置等下需要放入jwt中的数据 用一个map集合封装  

调用createToken方法 生成jwt处理完成后的token

该jwt基于SignatureAlgorithm.HS512 加密 密钥为

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第26张图片

 属于主模块admin中的yml配置中

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第27张图片

 最后回到SysLoginController中的login方法中 封装AjaxResult返回给前端

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第28张图片

异步线程类AsyncFactory 用于记录日志

recordLogininfor方法的返回值TimerTask

 先是创建了一个TimerTask对象 然后重写了其中的匿名内部类run()方法

该方法会创建一个子线程 内部获取用户的ip等信息 封装到SysLogininfor对象中

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第29张图片

AsyncManager这个类会调用.me方法 这个方法 会返回这个类的静态对象(饿汉模式加载 随着类加载而生成)

用这个me对象调用其execute(TimerTask task)方法 将上面创建的TimerTask对象传入 执行异步线程

这个方法中用成员变量ScheduledExecutorService 任务调度线程池执行schedule方法

使用了带延迟的时间调度,只执行一次

因为ScheduledExecutorService继承于ExecutorService,所以本身支持线程池的所有功能。额外还提供了4种方法,我们来看看其作用。
/**
 * 带延迟时间的调度,只执行一次
 * 调度之后可通过Future.get()阻塞直至任务执行完毕
 */
1. public ScheduledFuture schedule(Runnable command,
                                      long delay, TimeUnit unit);

/**
 * 带延迟时间的调度,只执行一次
 * 调度之后可通过Future.get()阻塞直至任务执行完毕,并且可以获取执行结果
 */
2. public ScheduledFuture schedule(Callable callable,
                                          long delay, TimeUnit unit);

/**
 * 带延迟时间的调度,循环执行,固定频率
 */
3. public ScheduledFuture scheduleAtFixedRate(Runnable command,
                                                 long initialDelay,
                                                 long period,
                                                 TimeUnit unit);

/**
 * 带延迟时间的调度,循环执行,固定延迟
 */
4. public ScheduledFuture scheduleWithFixedDelay(Runnable command,
                                                    long initialDelay,
                                                    long delay,
                                                    TimeUnit unit);

jwt认证过滤器

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第30张图片

获取token  解析获取其中的uuid  从redis缓存中取对应的数据 如果首次登录则是没有token的
如果不是首次登录 用户携带token过来 则刷新缓存

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第31张图片

因为SecurityContextHolder存入的是要一个authenticationToken对象 不能将loginUser直接存入 所以需要先创建一个authenticationToken对象 通过UsernamePasswordAuthenticationToken创建
需要传入三个参数第一个是loginUser 第二个先不填 第三个 loginUser.getAuthorities 是获取用户权限

 super.setAuthenticated(true); 代表是判断该用户是否是已认证的状态

大体流程 过滤器功能实现后放入spring容器后需要去配置 SpringSecurity的过滤器交给spring容器后并不会自动生效 整个流程由SpringSecurity自己管理 所以需要去SecurityConfig中的configure方法中配置

而JwtAuthenticationTokenFilter拦截器应该配置在UsernamePasswordAuthenticationFilter之前 形成过滤器链 下图红框这里

 Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第32张图片

使用httpSecurity.addFilterBefore设置 第一个参数 要添加的拦截器 第二个参数 在哪个拦截器之前 

permitAll策略 无论未登录还是已登录都可以访问
anonymous策略 匿名访问类型 未登录可以访问 已登录不可以访问

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity
            // CSRF禁用,因为不使用session
            .csrf().disable()
            // 认证失败处理类
            .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
            // 基于token,所以不需要session
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
            // 过滤请求
            .authorizeRequests()
            // 对于登录login 验证码captchaImage 允许匿名访问
            .antMatchers("/login", "/captchaImage").anonymous()
            .antMatchers(
                    HttpMethod.GET,
                    "/*.ttf",
                    "/*.woff",
                    "/*.gif",
                    "/*.eot",
                    "/*.json",
                    "/*.woff2",
                    "/*.png",
                    "/*.ico",
                    "/*.svg",
                    "/*.jpg",
                    "/*.html",
                    "/**/*.ttf",
                    "/**/*.woff",
                    "/**/*.gif",
                    "/**/*.eot",
                    "/**/*.json",
                    "/**/*.woff2",
                    "/**/*.png",
                    "/**/*.ico",
                    "/**/*.svg",
                    "/**/*.jpg",
                    "/**/*.html",
                    "/**/*.css",
                    "/**/*.js"
            ).permitAll().
                    antMatchers(
                    //mybatis复习相关的接口全部放行,同学们可以通过postMan进行测试而不需要进行权限认证
                    "/review/**",
                            "/review"
            ).permitAll()
            .antMatchers("/common/downloadByMinio**").permitAll()
            .antMatchers("/profile/**").anonymous()
            .antMatchers("/common/download**").anonymous()
            .antMatchers("/common/download/resource**").anonymous()
            .antMatchers("/webjars/**").anonymous()
            .antMatchers("/*/api-docs").anonymous()
            .antMatchers("/druid/**").anonymous()
            // 除上面外的所有请求全部需要鉴权认证
            .anyRequest().authenticated()
            .and()
            .headers().frameOptions().disable();
    httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
    // 添加JWT filter  成员属性注入JwtAuthenticationTokenFilter authenticationTokenFilter 设置配置JWT拦截器在UsernamePasswordAuthenticationFilter之后
    httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
    // 添加CORS filter
    httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);
    httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);
}

登出功能

本质是spring security底层帮我们处理的 是LogoutSuccessHandlerImpl这个实现类

LogoutSuccessHandlerImpl implements LogoutSuccessHandler

先从redis缓存中从redis缓存中拿用户信息 这里并没有从SecurityContextHolder中获取用户信息

//应该是可以从SecurityContextHolder中获取当前登录用户的信息的
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
LoginUser loginUser = (LoginUser) authentication.getPrincipal();

redis缓存用户登录信息的时间为30分钟     会存在用户信息过期 过期了应该让用户重新登录 而这时候去SecurityContextHolder中拿用户信息来做登出处理是否不合理? 这里时间不够并没有去具体理解

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第33张图片

不是空把redis缓存中的用户信息删除

 public void delLoginUser(String token)
{
    if (StringUtils.isNotEmpty(token))
    {
        String userKey = getTokenKey(token);
        redisCache.deleteObject(userKey);
    }
}

记录操作日志

如果从redis缓存中没拿到信息 说明用户已经连接超时了缓存已经过期 直接返回结果

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第34张图片

授权

授权基本流程

在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。

所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。

然后设置我们的资源所需要的权限即可。

在JwtAuthenticationTokenFilter中的doFilterInternal方法中

先通过tokenService.getLoginUser()方法拿带有权限信息的用户数据

将其封装到Authentication中给后面的权限校验器FilterSecurityInterceptor校验

 Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第35张图片

这其中的用户信息数据是在而在UserDetailsServiceImpl类中的
loadUserByUsername方法中通过userService.selectUserByUserName(username)去
SysUserServiceImpl类中的selectUserByUserName方法中查询

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第36张图片

 该sql查出当前user用户所有的信息以及dept部门信息和role角色信息 封装为一个SysUser对象

 

 

 

这样就获取了用户的基本信息 但是还需要用户的权限信息 通过createLoginUser方法去获取权限信息 调用了SysPermissionService类中的getMenuPermission方法去获取用户权限

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第37张图片 SysPermissionService类是用来处理用户权限的

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第38张图片

查询出用户的权限信息的sql根据sys_menu的menu_id关联sys_role_menu

通过sys_role_menu的role_id关联sys_user_role

通过sys_user_role的role_id关联sys_role

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第39张图片

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第40张图片

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第41张图片

最后创建一个LoginUser对象 为该对象设置两个成员属性 user用户属性 permissions 权限列表

 Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第42张图片

 Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第43张图片 

由于SpringSecurity是通过识别getAuthorities()这个方法中的权限 这个方法返回值是个泛型为Collection集合

需要找到一个这个GrantedAuthority接口的实现类设置权限permissions 这里按理应该这么做的 但是找不到 猜测并不是根据用户权限来设置 而是根据用户角色来设置权限的 我将new UsernamePasswordAuthenticationToken(loginUser, null, null)第三个选项设置为null后权限依然存在 所以猜测这里使用了角色鉴权

因为loginUser中已经封装了权限列表permissions 项目中的接口都通过自定义权限校验@PreAuthorize("@ss.hasPermi('xxx')")直接读取permissions 来判断权限

角色与权限

在spring security中,当用户登录成功后,当前登录用户信息将保存在Authentication对象中,该对象中有一个getAuthorities方法,用来返回当前对象所具备的权限信息,也就是已经授予当前登录用户的权限,getAuthorities方法返回值是Collection,即集合中存放的是GrantedAuthority的子类,当需要进行权限判断的时候,就会调用该方法获取用户的权限,进而做出判断。
无论用户的认证方式是用户名/密码形式、remember-me形式,还是其他如CAS、OAuth2等认证方式,最终用户的权限信息都可以通过getAuthorities方法获取。
那么对于Authentication#getAuthorities方法的返回值,应该如何理解:

从设计层面来讲,角色和权限是两个完全不同的东西:权限就是一些具体的操作,例如针对员工数据的读权限(READ_EMPLOYEE)和针对员工数据的写权限(WRITE_EMPLOYEE);角色则是某些权限的集合,例如管理员角色ROLE_ADMIN、普通用户角色ROLE_USER。
从代码层面来讲,角色和权限并没有太大的不同,特别是在spring security中,角色和权限的处理的方式基本上是一样的,唯一的区别在于spring security在多个地方会自动给角色添加一个ROLE_前缀,而权限则不会自动添加任何前缀。
至于Authentication#getAuthorities方法的返回值,则要分情况来对待:

如果权限系统设计比较简单,就是用户<=>权限<=>资源三者之间的关系,那么getAuthorities方法的含义就很明确,就是返回用户的权限。
如果权限系统设计比较复杂,同时存在角色和权限的概念,如用户<=>角色<=>权限<=>资源(用户关联角色、角色关联权限、权限关联资源),此时可以将getAuthorities方法的返回值当做权限来理解。由于spring security并未提供相关的角色类,因此这个时候需要自定义角色类。
如果系统同时存在角色和权限,可以使用GrantedAuthority的实现类来封装权限列表

但是找了整个CRM项目 暂时没找到GrantedAuthority的实现类 并且权限接口设置null依然可以获取权限,所以项目应该是没有根据GrantedAuthority来鉴权的

 Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第44张图片

 接口

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第45张图片

实现类

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第46张图片

授权实现

限制访问资源所需权限

SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。

但是要使用它我们需要先开启相关配置。

@EnableGlobalMethodSecurity

@EnableGlobalMethodSecurity 三方法详解

要开启Spring方法级安全,在添加了@Configuration注解的类上再添加@EnableGlobalMethodSecurity注解即可

其中注解@EnableGlobalMethodSecurity有几个方法:

  • prePostEnabled 确定 前置注解[@PreAuthorize,@PostAuthorize,..] 是否启用

  • securedEnabled 确定安全注解 [@Secured] 是否启用

  • jsr250Enabled 确定 JSR-250注解 [@RolesAllowed..]是否启用

在同一个应用程序中,可以启用多个类型的注解,但是只应该设置一个注解对于行为类的接口或者类。如:

  • 一个程序启用多个类型注解:

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true))
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
}

但是只应该设置一个注解对于行为类的接口或者类

public interface UserService {
  List findAllUsers();
 
  @PreAuthorize("hasAnyRole('user')")
  void updateUser(User user);
 
    // 下面不能设置两个注解,如果设置两个,只有其中一个生效
    // @PreAuthorize("hasAnyRole('user')")
  @Secured({ "ROLE_user", "ROLE_admin" })
  void deleteUser();
}

启用securedEnabled

public interface UserService {
    List findAllUsers();
    
    @Secured({"ROLE_user"})
    void updateUser(User user);
 
    @Secured({"ROLE_admin", "ROLE_user1"})
    void deleteUser();
}

 @Secured注解是用来定义业务方法的安全配置。在需要安全[角色/权限等]的方法上指定 @Secured,并且只有那些角色/权限的用户才可以调用该方法。

@Secured缺点(限制)就是不支持Spring EL表达式。不够灵活。并且指定的角色必须以ROLE_开头,不可省略。

在上面的例子中,updateUser 方法只能被拥有user权限的用户调用。deleteUser 方法只能够被拥有admin 或者user1 权限的用户调用。而如果想要指定"AND"条件,即调用deleteUser方法需同时拥有ADMINDBA角色的用户,@Secured便不能实现。

这时就需要使用prePostEnabled提供的注解@PreAuthorize/@PostAuthorize

启用prePostEnabled

public interface UserService {
    List findAllUsers();
 
    @PostAuthorize ("returnObject.type == authentication.name")
    User findById(int id);
 
    @PreAuthorize("hasRole('ADMIN')")
    void updateUser(User user);
    
    @PreAuthorize("hasRole('ADMIN') AND hasRole('DBA')")
    void deleteUser(int id);
}

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第47张图片

该注解更适合方法级的安全,也支持Spring 表达式语言,提供了基于表达式的访问控制。参见常见内置表达式了解支持表达式的完整列表

上面只使用到了一个注解@PreAuthorize,启用prePostEnabled后,提供有四个注解:

  • @PreAuthorize 进入方法之前验证授权。可以将登录用户的roles参数传到方法中验证。

    一些用法:

 // 只能user角色可以访问
@PreAuthorize ("hasAnyRole('user')")
// user 角色或者 admin 角色都可访问
@PreAuthorize ("hasAnyRole('user') or hasAnyRole('admin')")
// 同时拥有 user 和 admin 角色才能访问
@PreAuthorize ("hasAnyRole('user') and hasAnyRole('admin')")
// 限制只能查询 id 小于 10 的用户
@PreAuthorize("#id < 10")
User findById(int id);
 
// 只能查询自己的信息
 @PreAuthorize("principal.username.equals(#username)")
User find(String username);
 
// 限制只能新增用户名称为abc的用户
@PreAuthorize("#user.name.equals('abc')")
void add(User user)

@PostAuthorize 该注解使用不多,在方法执行后再进行权限验证。 适合验证带有返回值的权限。Spring EL 提供 返回对象能够在表达式语言中获取返回的对象returnObject。如:

// 查询到用户信息后,再验证用户名是否和登录用户名一致
@PostAuthorize("returnObject.name == authentication.name")
@GetMapping("/get-user")
public User getUser(String name){
    return userService.getUser(name);
}
// 验证返回的数是否是偶数
@PostAuthorize("returnObject % 2 == 0")
public Integer test(){
    // ...
    return id;
}

@PreFilter 对集合类型的参数执行过滤,移除结果为false的元素

// 指定过滤的参数,过滤偶数
@PreFilter(filterTarget="ids", value="filterObject%2==0")
public void delete(List ids, List username)

@PostFilter 对集合类型的返回值进行过滤,移除结果为false的元素

@PostFilter("filterObject.id%2==0")
public List findAll(){
    ...
    return userList;
}

对于前面使用@Secured注解的缺点,现在使用@PreAuthorize/@PostAuthorize

public interface UserService {
    List findAllUsers();
 
    @PostAuthorize ("returnObject.type == authentication.name")
    User findById(int id);
 
    @PreAuthorize("hasRole('ADMIN')")
    void updateUser(User user);
    
    @PreAuthorize("hasRole('ADMIN') AND hasRole('DBA')")
    void deleteUser(int id);
}

@preAuthorize 可使用 AND 和 or

表达式 描述
hasRole([role]) 当前用户是否拥有指定角色。
hasAnyRole([role1,role2]) 多个角色是一个以逗号进行分隔的字符串。如果当前用户拥有指定角色中的任意一个则返回true。
hasAuthority([auth]) 等同于hasRole
hasAnyAuthority([auth1,auth2]) 等同于hasAnyRole
Principle 代表当前用户的principle对象
authentication 直接从SecurityContext获取的当前Authentication对象
permitAll 总是返回true,表示允许所有的
denyAll 总是返回false,表示拒绝所有的
isAnonymous() 当前用户是否是一个匿名用户
isRememberMe() 表示当前用户是否是通过Remember-Me自动登录的
isAuthenticated() 表示当前用户是否已经登录认证成功了。
isFullyAuthenticated() 如果当前用户既不是一个匿名用户,同时又不是通过Remember-Me自动登录的,则返回true。

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第48张图片

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第49张图片

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第50张图片表达式判断 

自定义权限校验PermissionService

@Service("ss")加上注解以便让ioc容器接管

自定义的权限校验

从缓存中拿loginUser 判断是否有权限

Spring Security 结合jwt 自定义权限认证 基于huike CRM系统_第51张图片

你可能感兴趣的:(spring,系统安全,java)