SpringSecurity

一、初识SpringSecurity

1. 基本概念

  • Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。
  • 常用安全框架
    • Spring Security
    • Apache Shiro

2. 过滤器链

image-20230514142504331
img

3. 认证流程图

认证流程图
  • UsernamePasswordAuthenticationFilter
    • 负责处理我们在登录页面填写了用户名和密码之后的登录请求
  • ExceptionTranslationFilter
    • 处理过滤器链中跑出的任何 AccessDeniedExceptionAuthenticationException
  • FilterSecurityInterceptor
    • 负责权限校验的过滤器
  • Authentication :接口
    • 他的实现类表示当前访问系统的用户,封装与用户相关的所有信息
  • AuthenticationManager:接口
    • 定义了认证Authentication对象的方法
  • UserDetailsService:接口
    • 加载用户特定数据的核心接口,里面定义了一个loadUserByUsername()的方法
      • loadUserByUsername()
        • 根据用户名获取用户信息
        • 如果该用户存在,将查询到的用户信息封装到UserDetails对象返回
  • UserDetails:接口
    • 提供用户的核心信息,通过loadUserByUsername()接口返回的用户对象中的信息,将信息设置到Authentication对象中
  • Authentication:对象
    • 拥有权限列表信息的登录用户实体信息封装对象

4. 快速入门

img

4.1 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
</dependency>
<dependency>
    <groupId>com.baomidou</groupId>
    <artifactId>mybatis-plus-boot-starter</artifactId>
    <version>${mybatis-plus.version}</version>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.71</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

1.2 创建启动类

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class,args);
    }
}

1.3 简单yml配置

server:
  port: 80
  servlet:
    context-path: /
spring:
  application:
    name: demo
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql//localhost:3306/security
    username: root
    password: 996748

1.4 书写简单Controller

@RestController
public class TestController{
    
    @GetMapping("/test")
    public String test(){
        return "test";
    }
}

1.5 访问项目地址 localhost/test

  • 页面被重定向到SpringSecurity的默认登录页

  • 默认账号

    • 用户名:user
    • 密码:控制台打印密码
  • 登录成功后,可以查看到我们书写的简易controller返回信息test

二、SecurityConfig配置类

实际开发中我们不会把明文密码存入数据库中

默认使用的PassworEncoder要求数据库中的密码格式为{id}password,他会根据id去判断密码的加密方式,如果不写会出错,但是通常我们是不会采用这种方式的,所以我们需要替换默认的PasswordEncoder

我们只需要把要使用的BCryptPasswordEncoder注入Spring容器,SpringSecurity就会使用该编码器进行密码校验,我们可以定义一个SecurityConfig的配置类并继承WebSecurityConfigurerAdapter

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //编码器BCryptPasswordEncoder注入容器
    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    //认证管理器注入容器
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    
     @Override
    protected void configure(HttpSecurity http) throws Exception {
        //表单提交
        http.formLogin()
            	//添加表单参数映射,SpringSecurity默认只接受username和password参数,如果我们需要不同名称的参数,需要在此处指定名称映射
            	.usernameParameter("username")
            	.passwordParameter("password")
            	//指定登录请求服务地址,与表单提交地址一致 
            	.loginProcessingUrl("/login")
            	//自定义登录页面
            	.loginPage("/login.html")
            	//自定义成功页面,只支持POST方式,因而在前后端分离项目中无法使用
            	.successForwardUrl("/toIndex")
            	//自定义失败页面,只支持POST方式,因而在前后端分离项目中无法使用
            	.failureForwardUrl("/toError");
        //授权认证
        http.authorizeRequests()
            	//设置过滤器规则,增加页面与访问权限信息
            	.antMatchers("/login.html","/error.html").permitAll()
            	//混合项目中放行所有静态资源
            	.antMatchers("/js/**","/css/**","/images/**").permitAll()
            	.antMatchers("/admin.html").hasRole()
            	.anyRequest().authenticated();
        //关闭csrf防护
        http.csrf().disable();
    }
}

1. csrf

2. formLogin

  • 表单参数映射
    • usernameParameter():设置表单提交的username字段名称映射
    • passwordParameter():设置表单提交的password字段名称映射
  • 自定义请求与页面
    • loginProcessingUrl():指定自定义的登录请求地址
    • loginPage():指定自定义的登录页
    • successForwardUrl():指定自定义的登录成功后欲跳转的页面
    • failureForwardUrl():指定自定义的登录失败后欲跳转的页面

3. authorizeRequests

  • 访问权限控制
    • permitAll():无论是否登录均开放访问
    • denyAll():无论是否登录均不开放访问
    • anonymous():在未登录状态下,允许所有人访问
    • authenticated():在登录状态下,通过认证的可以访问
    • fullyAuthenticated():必须通过用户名密码直接的登录的用户才可以访问,勾选了记住我登录的用户无权访问
    • rememberMe():通过勾选了记住我登录的用户才可以访问
  • 访问权限判断
    • hasAuthority([权限名称])
    • hasAnyAuthority([权限列表,逗号分隔])
  • 访问角色判断
    • hasRole([角色名称])
    • hasAnyRole([角色列表,逗号分隔])
  • 访问IP判断
    • hasIpAddress([IP地址])

访问权限与访问角色是严格区分大小写的,不同的字母会导致不同角色的产生

3.1 antMatchers

  • 请求匹配器:可以为指定请求设置指定的访问权限

3.2 regexMatchers

  • 正则匹配器:使用正则表达式对指定格式的请求放行

3.3 mvcMatchers

application.yml配置文件中配置过如下信息的,可以使用mvcMatchers指定servletPath("/demo")

spring:
	mvc:
		servlet:
			path: /demo
http.authorizeRequests()
    	.mvcMatchers("/login").servletPath("/demo").permitAll();

3.4 anyRequests

  • 除了其他匹配器匹配的请求之外的所有请求
    • authenticated():登录认证后可访问

无论是antMatchers还是regexMatchers,他们都有两个参数的方法,可以使用这些方法来限定请求方式

  • antMatchers(HttpMethod method,String patterns)
  • regexMatchers(HttpMethod method,String regexPatterns)

4. exceptionHandling

  • accessDeniedHandler():设置访问拒绝的处理器

5. rememberMe

三、认证流程

SpringSecurity有对UserDetails默认的实现类User,用来作为请求以及返回的实体信息,我们可以通过自己实现UserDetails来创建我们自定义的请求返回实体信息,并且实现UserDetailsService来实现我们自定义的登录逻辑

1. 实现自定义的登录验证对象

1.1 自定义用户实体类

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserInfo {
    private String username;
    private String password;
}

1.2 实现UserDetails接口创建自定义的验证对象

@Data
public class LoginUser implements UserDetails {

    private UserInfo userInfo;
    private List<GrantedAuthority> authorities;

    public LoginUser(UserInfo userInfo, List<GrantedAuthority> authorities) {
        this.userInfo = userInfo;
        this.authorities = authorities;
    }

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

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

    @Override
    public String getUsername() {
        return userInfo.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;
    }
}

接下来我们需要自定义登录接口,然后让SpringSecurity对这个接口放行,用户在未登录前当然是没有权限的,所以我们应该将登录接口开放为所有人都可以访问

  1. 在接口中我们通过AuthenticationManager的authenticate方法来进行用户认证,所以需要在SecurityConfig中配置AuthenticationManager注入Spring容器
  2. 认证成功后要生成一个jwt(json-web-token),放入响应体返回,为了让用户下回请求能通过jwt识别出具体是哪个用户,我们需要把用户信息存入redis,以用户id作为作为key

2. 实现自定义的登录逻辑

@Service
public class UserDetailServiceImpl implements UserDetailsService {

    @Resource
    private UserInfoMapper userInfoMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<>();
        userInfoQueryWrapper.eq("username",username);
        UserInfo userInfo = userInfoMapper.selectOne(userInfoQueryWrapper);
        if(userInfo==null){
            throw new UsernameNotFoundException("用户名或密码错误");
        }
        return new LoginUser(userInfo);
    }
}

3. SecurityConfig

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭防护
        http.csrf().disable()
            //不通过Session获取SecurityContext
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            	//对于登录注册接口允许匿名访问
                .antMatchers("/user/login","/user/register").anonymous()
            	//除去以上所有配置过的请求,其余请求都需要鉴权认证
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/login")
                .loginPage("/login.html")
                .successHandler(new MyAuthenticationSuccessHandler("/admin"))
                .failureHandler(new MyAuthenticationFailureHandler("/error"));
            .and()
            .exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler);
    }
}

4. 实现自定义的登录服务对用户进行认证

@Service
public class LoginServiceImpl implements LoginService {

    @Resource
    private RedisCache redisCache;
    @Resource
    private AuthenticationManager authenticationManager;
    @Override
    public Response login(UserInfo userInfo){
        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userInfo.getUsername(), userInfo.getPassword());
        Authentication authenticate = authenticationManager.authenticate(usernamePasswordAuthenticationToken);
        if(authenticate==null){
            throw new RuntimeException("登录失败");
        }else {
            Object principal = authenticate.getPrincipal();
            if ((principal instanceof LoginUser)){
                LoginUser loginUser = (LoginUser) principal;
                String id = loginUser.getUserInfo().getId().toString();
                String username = loginUser.getUserInfo().getUsername();
                String jwt = JWTUtils.getJWT(id, username);
                Response response = new Response();
                response.code(ResponseCode.SUCCESS_CODE.getCode())
                        .message(ResponseCode.SUCCESS_CODE.getMessage())
                        .data(null)
                        .count(1)
                        .token(jwt);
                redisCache.setCacheObject("login:" + id,loginUser);
                return response;
            }
        }
        return null;
    }
}

5. 编写Controller

@RestController
@RequestMapping("/user")
public class LoginController {
    @Resource
    private LoginService loginService;
	@PostMapping("/login")
    public Response login(@RequestBody UserInfo userInfo){
        return loginService.login(userInfo);
    }
    @PostMapping("/logout")
    public Response logout(){
        return loginService.logout();
    }
}

6. 实现Token认证过滤器

@Component
public class JWTAuthenticationFilter extends OncePerRequestFilter {

    @Resource
    private RedisCache redisCache;
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        String token = httpServletRequest.getHeader("token");
        if(StringUtils.isEmpty(token)){
            filterChain.doFilter(httpServletRequest,httpServletResponse);
            return;
        }else {
            if(JWTUtils.isValid(token)){
                String uid = JWTUtils.getClaimInfoForString(httpServletRequest, "uid");
                Object cacheObject = redisCache.getCacheObject("login:" + uid);
                if (cacheObject instanceof LoginUser){
                    LoginUser loginUser  = (LoginUser) cacheObject;
                    if (loginUser==null){
                        return;
                    }else {
                        //TODO 未构建权限列表
                        UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, null);
                        SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                        filterChain.doFilter(httpServletRequest,httpServletResponse);
                    }
                }
            }
        }
        return;
    }
}

7. 更新SecurityConfig配置

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MyAccessDeniedHandler myAccessDeniedHandler;
    @Resource
    private JWTAuthenticationFilter jwtAuthenticationFilter;

    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭防护
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/user/login","/user/register").anonymous()
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/login")
                .loginPage("/login.html")
                .successHandler(new MyAuthenticationSuccessHandler("/successUrl"))
                .failureHandler(new MyAuthenticationFailureHandler("/failureUrl"))
            .and()
            .exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler)
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

8. 退出登录

@Override
public Response logout() {
    UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    Object principal = authentication.getPrincipal();
    if(principal instanceof LoginUser){
        LoginUser loginUser = (LoginUser) principal;
        String uid = loginUser.getUserInfo().getId().toString();
        redisCache.deleteObject("login:" + uid);
        Response response = new Response();
        response.code(ResponseCode.SUCCESS_CODE.getCode())
            .message("注销成功")
            .count(0)
            .data(null)
            .token("");
        return response;
    }
    return null;
}

四、前后端分离场景下登录认证跳转解决方案

SpringSecurity默认的认证成功跳转是一种混合项目的post请求,但是目前越来越多的前后端分离项目涌现或者说在前后端分离开发变成一种规范的当下,这种形式已经不再适用我们的开发要求了,所以我们需要自定义SpringSecurity认证成功重定向的处理器,我们通过实现AuthenticationSuccessHandler AuthenticationFailureHandler接口来自定义我们认证成功或失败后的重定向方式

1. 重写认证授权成功处理器

//实现AuthenticationSuccessHandler接口完成请求转发或重定向操作,对应登录成功后的操作
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private String successUrl;

    public MyAuthenticationSuccessHandler(String successUrl) {
        this.successUrl = successUrl;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.sendRedirect(successUrl);
    }
}

2. 重写认证授权失败处理器

//实现AuthenticationFailureHandler接口完成请求转发或重定向操作,对应登录失败后的操作
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private String failureUrl;

    public MyAuthenticationSuccessHandler(String failureUrl) {
        this.failureUrl = failureUrl;
    }

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.sendRedirect(failureUrl);
    }
}

3. 配置自定义处理器

//使用我们实现的SuccessHandler和FailureHandler设置登录成功和失败后的请求重定向
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/login")
                .loginPage("/login.html")
            	//此处使用我们自己实现的重定向处理器,这样就可以支持前后端分离了
                .successHandler(new MyAuthenticationSuccessHandler("/admin"))
                .failureHandler(new MyAuthenticationFailureHandler("/error"));
        http.authorizeRequests()
                .antMatchers("/login.html","/error.html").permitAll()
                .antMatchers("/admin.html").hasRole("ADMIN")
                .anyRequest().authenticated();

        http.csrf().disable();
    }
}

五、授权流程

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

所以我们需要将当前登录用户的权限信息存入Authentication

然后将对应的资源设置好访问权限

如果满足角色条件,程序正常执行,如果不满足,抛出 org.springframework.security.access.AccessDeniedException

1. 授权实现

1.1 限制访问资源所需权限

  • @Secured

    • 专门用于判断是否具有指定角色权限,可用在方法上或类上,与hasRole方法相反,参数要以ROLE_开头

    • 使用方式

      • @SpringBootApplication
        //开启注解访问控制
        @EnableGlobalMethodSecurity(SecuredEnabled = true)
        public class SpringSecurityDemoApplication {
        
            public static void main(String[] args) {
                SpringApplication.run(SpringSecurityDemoApplication.class, args);
            }
        }
        
      • @Secured("ROLE_ADMIN")
        @GetMapping("/admin")
        public String toAdmin(){
            return "redirect:admin.html";
        }
        
      • 此时当我们访问到/admin时,就会检测用户是否登录成功,如果登录成功判断用户是否存在角色权限,如果存在访问成功,否则500

  • @PreAuthorize@PostAuthorize

    • @PreAuthorize 表示在访问方法或类之前先判断权限,注解参数和access方法参数格式相同,都为权限表达式

    • @PostAuthorize 表示在访问方法或类之后判断权限,很少用

    • 使用方式

      • @SpringBootApplication
        //开启注解访问控制
        @EnableGlobalMethodSecurity(prePostEnable = true)
        public class SpringSecurityDemoApplication {
        
            public static void main(String[] args) {
                SpringApplication.run(SpringSecurityDemoApplication.class, args);
            }
        }
        
      • @PreAuthorize("hasRole(ADMIN)")//PreAuthorize允许角色ROLE_开头
        @GetMapping("/admin")
        public String toAdmin(){
            return "redirect:admin.html";
        }
        

1.2 封装权限信息

事实上我之前在UserDetailsServiceImpl中不仅需要查询出数据库中的用户信息,而且还要查询出对应的权限信息,一并封装到UserDetails中返回,我们自己实现了UserDetails为LoginUser

  • @Data
    @NoArgsConstructor
    public class LoginUser implements UserDetails {
    
        private UserInfo userInfo;
        private List<String> roleList;
        @JSONField(serialize = false)
        private HashSet<GrantedAuthority> authorities;
    
        public LoginUser(UserInfo userInfo, List<String> roleList) {
            this.userInfo = userInfo;
            this.roleList = roleList;
        }
    
        public LoginUser(UserInfo userInfo) {
            this.userInfo = userInfo;
        }
    
        @Override
        public Collection<? extends GrantedAuthority> getAuthorities() {
            if(authorities!=null && authorities.size()!=0){
                return authorities;
            }
            for (String role : roleList) {
                authorities.add(new SimpleGrantedAuthority(role));
            }
            //authorities = roleList.stream().map(SimpleGrantedAuthority::new).collect(Collectors.toList());
            return authorities;
        }
    
        @Override
        public String getPassword() {
            return userInfo.getPassword();
        }
    
        @Override
        public String getUsername() {
            return userInfo.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;
        }
    }
    
  • @Service
    public class UserDetailServiceImpl implements UserDetailsService {
    
        @Resource
        private UserInfoMapper userInfoMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            QueryWrapper<UserInfo> userInfoQueryWrapper = new QueryWrapper<>();
            userInfoQueryWrapper.eq("username",username);
            UserInfo userInfo = userInfoMapper.selectOne(userInfoQueryWrapper);
            if(userInfo==null){
                throw new UsernameNotFoundException("用户名或密码错误");
            }
            List<String> roleList = Arrays.asList("admin","user");
            return new LoginUser(userInfo,roleList);
        }
    }
    
  • @Component
    public class JWTAuthenticationFilter extends OncePerRequestFilter {
    
        @Resource
        private RedisCache redisCache;
        @Override
        protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            String token = httpServletRequest.getHeader("token");
            if(StringUtils.isEmpty(token)){
                filterChain.doFilter(httpServletRequest,httpServletResponse);
                return;
            }else {
                if(JWTUtils.isValid(token)){
                    String uid = JWTUtils.getClaimInfoForString(httpServletRequest, "uid");
                    Object cacheObject = redisCache.getCacheObject("login:" + uid);
                    if (cacheObject instanceof LoginUser){
                        LoginUser loginUser  = (LoginUser) cacheObject;
                        if (loginUser==null){
                            return;
                        }else {
                            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
                                    new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
                            SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
                            filterChain.doFilter(httpServletRequest,httpServletResponse);
                        }
                    }
                }
            }
            return;
        }
    }
    

2. 基于角色的权限控制(RBAC)

2.1 设计数据表

  • 设计权限表

    DROP TABLE IF EXISTS `sys_menu`;
    CREATE TABLE `sys_menu`  (
      `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
      `menu_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NOT NULL COMMENT '菜单名称即权限名称',
      `routing_address` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '路由地址即服务地址',
      `component_path` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '组件路径',
      `visible` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '菜单状态(0显示,1隐藏)',
      `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '功能状态(0正常,1停用)',
      `perms` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '权限标识',
      `icon` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '#' COMMENT '菜单图标',
      `create_by` bigint NULL DEFAULT NULL COMMENT '创建自',
      `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
      `update_by` bigint NULL DEFAULT NULL COMMENT '更新自',
      `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
      `del_flag` int NULL DEFAULT NULL COMMENT '是否删除(0未删除,1已删除)',
      `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '菜单(权限)表' ROW_FORMAT = Dynamic;
    
    SET FOREIGN_KEY_CHECKS = 1;
    
  • 设计角色表

    DROP TABLE IF EXISTS `sys_role`;
    CREATE TABLE `sys_role`  (
      `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
      `role_name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色名称',
      `role_key` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '角色权限字符串',
      `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '角色状态(0正常,1停用)',
      `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '是否删除(0未删除,1已删除)',
      `create_by` bigint NULL DEFAULT NULL COMMENT '创建自',
      `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
      `update_by` bigint NULL DEFAULT NULL COMMENT '更新自',
      `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
      `remark` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '备注',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '角色表' ROW_FORMAT = Dynamic;
    
    SET FOREIGN_KEY_CHECKS = 1;
    
  • 设计角色-权限表

    DROP TABLE IF EXISTS `sys_role_menu`;
    CREATE TABLE `sys_role_menu`  (
      `role_id` bigint NOT NULL COMMENT '角色ID',
      `menu_id` bigint NOT NULL DEFAULT 0 COMMENT '菜单ID',
      PRIMARY KEY (`role_id`, `menu_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
    
    SET FOREIGN_KEY_CHECKS = 1;
    
  • 设计用户表

    DROP TABLE IF EXISTS `sys_user`;
    CREATE TABLE `sys_user`  (
      `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
      `user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户名',
      `nick_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '昵称',
      `password` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '密码',
      `status` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '账号状态(0正常,1停用)',
      `email` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '邮箱',
      `phone_number` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '手机号',
      `gender` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户性别(0男,1女,2未知)',
      `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '头像地址',
      `user_type` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT NULL COMMENT '用户类型(0管理员,1普通用户)',
      `create_by` bigint NULL DEFAULT NULL COMMENT '创建自',
      `create_time` datetime NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
      `update_by` bigint NULL DEFAULT NULL COMMENT '更新自',
      `update_time` datetime NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
      `del_flag` char(1) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci NULL DEFAULT '0' COMMENT '是否删除(0未删除,1已删除)',
      PRIMARY KEY (`id`) USING BTREE
    ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_0900_ai_ci COMMENT = '用户表' ROW_FORMAT = Dynamic;
    
    SET FOREIGN_KEY_CHECKS = 1;
    
  • 设计用户-角色表

    DROP TABLE IF EXISTS `sys_user_role`;
    CREATE TABLE `sys_user_role`  (
      `user_id` bigint NOT NULL COMMENT '用户ID',
      `role_id` bigint NOT NULL COMMENT '角色ID',
      PRIMARY KEY (`user_id`, `role_id`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
    
    SET FOREIGN_KEY_CHECKS = 1;
    

2.2 从数据库中查询权限信息

六、access方法

access()方法中填写权限表达式,具体权限表达式如下表,另外支持自定义权限表达式

1. SpringSecurity规定一些常用表达式

表达式 说明
hasRole([role]) 用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀)
hasAnyRole([role1,role2]) 用户拥有任意一个制定的角色时返回true
hasAuthority([authority]) 等同于hasRole,但不会带有ROLE_前缀
hasAnyAuthority([auth1,auth2]) 等同于hasAnyRole
permitAll 永远返回true
denyAll 永远返回false
authentication 当前登录用户的authentication对象
fullAuthenticated 当前用户既不是anonymous也不是rememberMe用户时返回true
hasIpAddress('192.168.1.0/24') 请求发送的IP匹配时返回true

2. 自定义表达式

2.1 创建自定义接口

public interface MyAccessExpression {
    boolean hasPermission(HttpServletRequest request, Authentication authentication);
}

2.2 实现自定义接口

@Component("myEX")
public class MyAccessExpressionImpl implements MyAccessExpression {
    @Override
    public boolean hasPermission(String roleName) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        Object principal = authentication.getPrincipal();
        if(principal instanceof LoginUser){
            LoginUser loginUser = (LoginUser) principal;
            Collection<? extends GrantedAuthority> authorities = loginUser.getAuthorities();
            return authorities.contains(roleName);
        }
        return false;
    }
}

2.3 在SecurityConfig中配置自定义表达式

.anyRequest().access("@myEx.hasPermission('sys:file:delete')") 

2.4 使用注解调用自定义表达式

@PreAuthorize("@myEX.hasPermission('sys:file:delete')")

七、自定义403处理方案

当用户请求认证失败或者是授权失败时,SpringSecurity会使用一些默认处理,会在前端展示一些状态信息,但这通常不是我们希望前端开发者以及用户看到的,所以我们应该给前端返回统一的response格式,让前端自行决定展示内容

  • 认证失败的异常通常会由AuthenticationException调用AuthenticationEntryPoint对象的commence方法来处理
  • 授权失败的异常通常会由AccessDeniedException调用AccessDeniedHandler对象的handle方法来处理

所以如果我们需要拦截这些默认的返回信息,就需要自定义AuthenticationEntryPointAccessDeniedHandler的实现类,重写它们对应的处理方法,即可返回我们需要的统一response格式

1. 自定义认证失败处理

1.1 实现AuthenticationEntryPoint接口

@Component
public class AuthenticationEntryPointHandler implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        httpServletResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        httpServletResponse.setContentType("application/json");
        httpServletResponse.setCharacterEncoding("UTF-8");
        PrintWriter writer = httpServletResponse.getWriter();
        writer.println(JSON.toJSONString(
                new Response<String>().code(ResponseCode.UN_AUTHORIZATION_CODE.getCode())
                        .message(ResponseCode.UN_AUTHORIZATION_CODE.getMessage())
                        .count(0)
                        .data("对不起,认证失败")
        ));
        writer.flush();
        writer.close();
    }
}

1.2 配置自定义认证失败处理器

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MyAccessDeniedHandler myAccessDeniedHandler;
    @Resource
    private JWTAuthenticationFilter jwtAuthenticationFilter;
    @Resource
    private AuthenticationEntryPointHandler authenticationEntryPointHandler;

    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭防护
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/user/login","/user/register").anonymous()
                .anyRequest().authenticated()
                .anyRequest().access("@myAccessExpressionImpl.hasPermission(request,authentication)")
            .and()
            .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/login")
                .loginPage("/login.html")
                .successHandler(new MyAuthenticationSuccessHandler("/successUrl"))
                .failureHandler(new MyAuthenticationFailureHandler("/failureUrl"))
            .and()
            .exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPointHandler)
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

2. 自定义授权失败处理

2.1 实现AccessDeniedHandler接口

@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException {
        httpServletResponse.setStatus(HttpServletResponse.SC_FORBIDDEN);
        httpServletResponse.setContentType("application/json");
        httpServletResponse.setCharacterEncoding("UTF-8");
        PrintWriter writer = httpServletResponse.getWriter();
        writer.println(JSON.toJSONString(
                new Response<String>().code(ResponseCode.FORBIDDEN_CODE.getCode())
                        .message(ResponseCode.FORBIDDEN_CODE.getMessage())
                        .count(0)
                        .data("对不起,您无权访问此页面")
        ));
        writer.flush();
        writer.close();
    }
}

2.2 配置自定义授权失败处理器

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MyAccessDeniedHandler myAccessDeniedHandler;
    @Resource
    private JWTAuthenticationFilter jwtAuthenticationFilter;
    @Resource
    private AuthenticationEntryPointHandler authenticationEntryPointHandler;

    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭防护
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/user/login","/user/register").anonymous()
                .anyRequest().authenticated()
                .anyRequest().access("@myAccessExpressionImpl.hasPermission(request,authentication)")
            .and()
            .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/login")
                .loginPage("/login.html")
                .successHandler(new MyAuthenticationSuccessHandler("/successUrl"))
                .failureHandler(new MyAuthenticationFailureHandler("/failureUrl"))
            .and()
            .exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPointHandler)
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}

八、RememberMe功能实现

SpringSecurity中想要实现记住我功能,用户只需要在登录时添加value为remember-me的checkbox。取值为true,SpringSecurity会自动把用户信息存储到数据源中,以后就不用再使用用户名密码登录访问

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;
    @Resource
    private DataSource dataSource;
    @Resource
    private PersistentTokenRespository persistentTokenRespository;
    
    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

	@Bean
    public PersistentTokenRespository getPersistentTokenRespository(){
        JdbcTokenRespositoryImpl jsbcTokenRespository = new JdbcTokenRespositoryImpl();
        jsbcTokenRespository.setDataSource(dataSource);
        //在数据库中创建表,首次创建后,无需再次创建
        jsbcTokenRespository.setCreateTableOnStartup(true);
        return jsbcTokenRespository;
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
		http.rememberMe()
            	//token失效时间,单位秒
            	.tokenValiditySeconds(60*60*24*7)
            	//绑定参数名称,默认为“remember-me”
            	.rememberMeParameter("rememberMe")
            	//自定义登录逻辑
            	.userDetailsService(userDetailsServiceImpl)
            	//持久层对象
            	.tokenRespository(persistentTokenRespository);
    }
}

九、退出登录处理器

1. 书写退出登录代码

@Override
public Response logout() {
    UsernamePasswordAuthenticationToken authentication = (UsernamePasswordAuthenticationToken) SecurityContextHolder.getContext().getAuthentication();
    Object principal = authentication.getPrincipal();
    if(principal instanceof LoginUser){
        LoginUser loginUser = (LoginUser) principal;
        String uid = loginUser.getUserInfo().getId().toString();
        redisCache.deleteObject("login:" + uid);
        Response response = new Response();
        response.code(ResponseCode.SUCCESS_CODE.getCode())
            .message("注销成功")
            .count(0)
            .data(null)
            .token("");
        return response;
    }
    return null;
}

2. 实现退出登录处理器

@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        System.out.println("退出登录成功");
    }
}

3. 配置自定义退出登录处理器

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private MyAccessDeniedHandler myAccessDeniedHandler;
    @Resource
    private JWTAuthenticationFilter jwtAuthenticationFilter;
    @Resource
    private AuthenticationEntryPointHandler authenticationEntryPointHandler;
    @Resource
    private MyLogoutSuccessHandler myLogoutSuccessHandler;

    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }

    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //关闭防护
        http.csrf().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
                .antMatchers("/user/login","/user/register").anonymous()
                .anyRequest().authenticated()
                .anyRequest().access("@myAccessExpressionImpl.hasPermission(request,authentication)")
            .and()
            .formLogin()
                .usernameParameter("username")
                .passwordParameter("password")
                .loginProcessingUrl("/login")
                .loginPage("/login.html")
                .successHandler(new MyAuthenticationSuccessHandler("/successUrl"))
                .failureHandler(new MyAuthenticationFailureHandler("/failureUrl"))
            .and()
            .exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler)
                .authenticationEntryPoint(authenticationEntryPointHandler)
            .and()
            .addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
            .cors()
            .and()
            .logout()
                .logoutSuccessHandler(myLogoutSuccessHandler);

    }
}

十、CSRF和CORS

CSRF:跨站请求伪造,也被称为 OneClick Attack或者Session Rifing,通过伪造用户请求访问受信任站点的非法请求访问。

CORS:跨域资源共享,只要网络协议、IP地址、端口中的任何一个不相同就是跨域请求

1. SpringBoot开启跨域

@Configuration
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedMethods("GET","POST","DELETE","PUT","OPTION")
                .allowedHeaders("*")
                .maxAge(3600);
    }
}

2. SpringSecurity开启跨域

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private UserDetailsService userDetailsService;
    
    @Bean
    public PasswordEncoder getPasswordEncoder(){
        return new BCryptPasswordEncoder();
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	//CSRF防护默认是开启的
        //关闭CSRF防护
        http.csrf().disable();
        //允许跨域
        http.cors();
    }
}

当CSRF防护开启时,前端提交登录请求参数必须携带_csrf参数并提交对应token值,后端会验证token是否为后端生成的令牌,确认通过后即可进行登录操作

混合开发中,应该将token值写入隐藏表单域并跟随表单数据一同提交

前后端分离开发中,应该将令牌设置到RequestHeader中与JSON请求体一同提交

十一、Oauth2协议

img

简介:第三方认证技术方案最主要是用来解决认证协议的通用标准问题,因为要实现跨系统认证,各系统之间一定要遵循一定的接口协议

Oauth协议为用户资源的授权提供了一个安全的、开放而又简易的标准。同时,任何第三方都可以使用Oauth认证服务,人和服务提供商都可以实现自身的Oauth认证服务,业界提供了Oauth的多种语言(PHP、JavaScript、Java、Ruby等)实现的SDK,大大节约我们的开发时间。

Oauth目前发展到2.0版本,已得到广泛应用

1. 基本概念

  • 常用术语

    • 客户凭证(client credentials):客户端的clientId和密码用于认证客户

    • 令牌(tokens):授权服务器在接受到客户请求后,颁发的访问令牌

    • 作用域(scopes):客户请求访问令牌时,由资源拥有者额外指定的细分权限(permission)

  • 令牌类型

    • 授权码:仅用于授权码授权类型,用于交换获取访问令牌和刷新令牌
    • 访问令牌:用于代表一个用户或服务直接去访问受保护的资源
    • 刷新令牌:用于去授权服务器获取一个刷新访问令牌
    • BearerToken:不管谁拿到Token都可以访问资源
    • Proof of Possession Token:可以校验client是否对Token由明确的一拥有权
  • 优点

    • 更安全,客户端不接触用户密码,服务器更易集中保护
    • 广泛传播被持续采用
    • 短寿命和封装的token
    • 资源服务器与授权服务器解耦
    • 集中式授权,简化客户端
    • 易于请求和传递
    • 考虑多种客户端架构场景
    • 客户可以具有不同的信任级别
  • 缺点:

    • 协议框架太宽泛,造成各种实现的兼容性和互操作性差
    • 不是一个认证协议,本身并不能告诉你任何用户信息

2. 授权模式

①. 授权码模式

image-20230514185203694

②. 简化授权模式

image-20230514185306983

③. 密码模式

image-20230514185427162

④. 客户端模式

image-20230514185524440

⑤. 刷新令牌

image-20230514185619563

3. SpringSecurity Oauth2架构

image-20230514185755510

①. 授权服务器

  • Authorize Endpoint:授权端点,进行授权
  • Token Endpoint:令牌端点,经过授权拿到对应token
  • Introspection Endpoint:校验端点,校验token合法性
  • Revocation Endpoint:撤销端点,撤销授权
    SpringSecurityOauth2

②. SpringSecurityOauth2流程

  1. 用户访问此时没有Token,Oauth2RestTemplate会报错,这个报错信息会被Oauth2ClientContextFilter捕获

  2. 认证服务器通过Authorization Endpoint进行授权,并通过AuthorizationServerTokenServices生成授权码并返回给客户端

  3. 客户端拿到授权码去认证服务器通过Token Endpoint调用AuthorizationServerTokenServices生成Token并返回给客户端

  4. 客户端拿到Token去资源服务器去访问资源,一般会通过Oauth2AuthenticationManager调用ResourceServerTokenServices进行校验,校验通过可以获取资源

③. 快速入门

  • 引入依赖

    <!--管理spring-cloud版本-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR12</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <!--引入对应依赖-->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    
    image-20230514200005489
    image-20230514200029459
    image-20230514200146473
image-20230514192606549
image-20230514192736465

十二、JWT

1. 常见的认证机制

  • HTTP Basic Auth
  • Cookie Auth
  • OAuth
  • Token Auth

2. Token Auth

  • 优点
    • 支持跨域访问
    • 无状态,服务端无需存储session信息,只需在客户端cookie中存储token值
    • 更适用于CDN
    • 解耦,不需要绑定到一个特定的身份验证方案
    • 适用于移动端应用,原生的移动应用是不支持cookie的
    • CSRF:不再依赖Cookie,所以不需要考虑CSRF的防护
    • 性能相比于CookieAuth更快,因为只需要对token进行验证和解析
    • 不需要再为登录页面做特殊处理
    • 基于标准化
    • 跨语言
    • 轻量级JSON风格参数
  • 缺点
    • 无法更新token有效期
    • 无法销毁一个token

3. 什么是JWT

JSON Web Token是一个开放的行业标准(RFC7519),它定义了一种简洁的、自包含的协议格式,用于在统信双方传递json对象,传递信息经过数字签名可以被验证和信任,JWT可以使用HMAC算法或使用RSA的公钥/私钥对来签名,防止被篡改

  • 官网:https://jwt.io
  • 标准:https://tools.ietf.org/html/rfc7519

3.1 JWT的优点

  1. jwt基于json,非常方便解析
  2. 可以在令牌中自定义丰富的内容,易扩展
  3. 通过非对称加密算法及数字签名技术,可以防篡改,安全性高
  4. 资源服务使用JWT可以不依赖认证服务完成授权

4. JWT组成

一个JWT实际上就是一个字符串,它由三部分组成,头部、负载与签名。

header(base64).payload(base64).singature(HS256)

4.1 头部

用于描述关于当前JWT的最基本信息,例:签名所使用的算法

{
    "alg":"HS256",
    "typ":"JWT"
}
  • typ:类型
  • alg:签名算法

Base64是一种基于64个可打印字符来表示二进制数据的表示方法。由于2的6次方等于64,所以每6bit为一个单元,对应某个可打印字符。3个字节有24bit,对应4个Base64单元,即3个字节需要用4个可打印字符来表示,JDK中提供了非常方便的BASE64Encoder和BASE64Decoder,用他们可以非常方便的完成BASE64的编码和解码。BASE64是一个对称编码,所以JWT头部信息在被BASE64编码后并不安全

4.2 负载(Payload)

用于存放有效信息,其实就是内容,内容包含以下3个部分

  • 标准中注册的声明

    • iss:jwt签发者

    • sub:jwt所面向的用户

    • aud:接收jwt的一方

    • exp:jwt过期时间,必须大于签发时间

    • nbf:定义在何时之前jwt是不可用的

    • iat:jwt签发时间

    • jti:jwt的唯一身份标识,用来作为一次性token,回避重放攻击

  • 公共的声明

    • 可以添加任何信息,一般用来添加用户信息或业务信息,不要存放敏感信息,因为客户端可解密
  • 私有的声明

    • 是提供者和消费者共同定义的声明,一般不建议存放敏感信息,因为客户端可解密

4.3 签证(Signature)

jwt的第三部分是一个签证信息,签证信息由以下三个部分组成:

  • header(BASE64加密)
  • payload(BASE64加密)
  • secret(秘钥)
    • secret是存储在服务器上的,jwt的签发也是在服务器端,secret就是用来进行jwt的签发和jwt的验证的,这个秘钥是保密的,如果泄露,客户端就可以自行签发jwt

5. 快速入门

5.1 引入依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

5.2 书写工具类

package com.zhiyuan.security.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

import static org.springframework.util.StringUtils.isEmpty;

/**
 * @author 絷缘
 * @date 2023-05-14
 * @Description JWT工具类
 */
public class JWTUtils {

    //默认的Token过期时间
    public static final Long EXPIRE_TIME = 7*24*60*60L;
    //JWT秘钥
    public static final String SECRET = "4C5C8A178EE84933B04D21249185EDEB";
    public static final String SEPARATOR_EMPTY = "";
    public static final String SEPARATOR_SHORT_LINE = "-";

    /**
     * 生成UUID
     * @param upperCase 是否转大写
     * @return 返回UUID字符串
     */
    public static String UUID(boolean upperCase){
        String UUID_STR = UUID.randomUUID().toString().replace(SEPARATOR_SHORT_LINE, SEPARATOR_EMPTY);
        if(upperCase){
            return UUID_STR.toUpperCase();
        }
        return UUID_STR.toLowerCase();
    }
    /**
     * 生成JWT
     * @param uid 负载信息userId
     * @param username 负载信息username
     * @return 返回JWT字符串
     */
    public static String generateJWT(String uid,String username){
        HashMap<String, Object> headerMap = new HashMap<>();
        headerMap.put("alg","HS256");
        headerMap.put("typ","JWT");
        HashMap<String, Object> claimMap = new HashMap<>();
        claimMap.put("uid",uid);
        claimMap.put("username",username);
        String jwt = Jwts.builder()
                //设置头部信息
                .setHeader(headerMap)
                //设置负载信息
                .setIssuer("zhiyuan")
                .setId(UUID(true))
                .setSubject("cloud-drive")
                .setIssuedAt(new Date())
                .setClaims(claimMap)
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE_TIME))
                //设置签名信息
                .signWith(SignatureAlgorithm.HS256, SECRET)
                .compact();
        return jwt;
    }

    /**
     * 生成JWT
     * @param id 传入jti
     * @param subject 传入sub
     * @param issuedAt 传入iat
     * @param claimMap  传入自定义声明claim
     * @param expireTime 传入exp过期时间
     * @return 返回JWT字符串
     */
    public static String generateJWT(String id, String subject, Date issuedAt,Map claimMap,Long expireTime){
        HashMap<String, Object> headerMap = new HashMap<>();
        headerMap.put("alg","HS256");
        headerMap.put("typ","JWT");
        String jwt = Jwts.builder()
                //设置头部信息
                .setHeader(headerMap)
                //设置负载信息
                .setIssuer("zhiyuan")
                .setId(id)
                .setSubject(subject)
                .setIssuedAt(issuedAt)
                .setClaims(claimMap)
                .setExpiration(new Date(System.currentTimeMillis() + expireTime))
                //设置签名信息
                .signWith(SignatureAlgorithm.HS256, SECRET)
                .compact();
        return jwt;
    }

    /**
     * 验证Token是否有效
     * @param jwt 传入jwt字符串
     * @return 有效返回true,无效返回false
     */
    public static boolean isValid(String jwt){
        if (isEmpty(jwt)){
            return false;
        }
        try{
            Jwts.parser().setSigningKey(SECRET).parseClaimsJws(jwt);
        }catch (Exception e){
            System.err.println(e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * 验证Token是否有效
     * @param request 传入HttpServletRequest对象
     * @return 有效返回true,无效返回false
     */
    public static boolean isValid(HttpServletRequest request){
        String token = request.getHeader("token");
        if(isEmpty(token)){
            return false;
        }
        try{
            Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
        }catch (Exception e){
            System.err.println(e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * 获取Token负载内容字符串
     * @param request 传入HttpServletRequest对象
     * @param claimName 传入负载内容key
     * @return 返回负载内容key对应的负载内容value
     */
    public static String getClaimInfoForString(HttpServletRequest request,String claimName){
        String token = request.getHeader("token");
        if(isEmpty(token)){
            return "token不存在";
        }
        try{
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
            Claims body = claimsJws.getBody();
            return body.get(claimName,String.class);
        }catch (Exception e){
            System.err.println(e.getMessage());
            return "token已失效";
        }
    }

    /**
     * 获取Token负载内容对象
     * @param request 传入HttpServletRequest对象
     * @param claimName 传入负载内容key
     * @param tClass 传入负载内容value类型
     * @return 返回负载内容key对应的负载内容value
     * @param <T>
     */
    public static <T> T getClaimInfoForObject(HttpServletRequest request,String claimName,Class<T> tClass) {
        String token = request.getHeader("token");
        if(isEmpty(token)){
            return null;
        }
        try{
            Jws<Claims> claimsJws = Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token);
            Claims body = claimsJws.getBody();
            return body.get(claimName,tClass);
        }catch (Exception e){
            System.err.println(e.getMessage());
            return null;
        }
    }

}

5.3 Redis工具类

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.BoundSetOperations;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.stereotype.Component;

/**
 * spring redis 工具类
 **/
@SuppressWarnings(value = { "unchecked", "rawtypes" })
@Component
public class RedisCache
{
    @Autowired
    public RedisTemplate redisTemplate;

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @return 缓存的对象
     */
    public <T> ValueOperations<String, T> setCacheObject(String key, T value)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        operation.set(key, value);
        return operation;
    }

    /**
     * 缓存基本的对象,Integer、String、实体类等
     *
     * @param key 缓存的键值
     * @param value 缓存的值
     * @param timeout 时间
     * @param timeUnit 时间颗粒度
     * @return 缓存的对象
     */
    public <T> ValueOperations<String, T> setCacheObject(String key, T value, Integer timeout, TimeUnit timeUnit)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        operation.set(key, value, timeout, timeUnit);
        return operation;
    }

    /**
     * 获得缓存的基本对象。
     *
     * @param key 缓存键值
     * @return 缓存键值对应的数据
     */
    public <T> T getCacheObject(String key)
    {
        ValueOperations<String, T> operation = redisTemplate.opsForValue();
        return operation.get(key);
    }

    /**
     * 删除单个对象
     *
     * @param key
     */
    public void deleteObject(String key)
    {
        redisTemplate.delete(key);
    }

    /**
     * 删除集合对象
     *
     * @param collection
     */
    public void deleteObject(Collection collection)
    {
        redisTemplate.delete(collection);
    }

    /**
     * 缓存List数据
     *
     * @param key 缓存的键值
     * @param dataList 待缓存的List数据
     * @return 缓存的对象
     */
    public <T> ListOperations<String, T> setCacheList(String key, List<T> dataList)
    {
        ListOperations listOperation = redisTemplate.opsForList();
        if (null != dataList)
        {
            int size = dataList.size();
            for (int i = 0; i < size; i++)
            {
                listOperation.leftPush(key, dataList.get(i));
            }
        }
        return listOperation;
    }

    /**
     * 获得缓存的list对象
     *
     * @param key 缓存的键值
     * @return 缓存键值对应的数据
     */
    public <T> List<T> getCacheList(String key)
    {
        List<T> dataList = new ArrayList<T>();
        ListOperations<String, T> listOperation = redisTemplate.opsForList();
        Long size = listOperation.size(key);

        for (int i = 0; i < size; i++)
        {
            dataList.add(listOperation.index(key, i));
        }
        return dataList;
    }

    /**
     * 缓存Set
     *
     * @param key 缓存键值
     * @param dataSet 缓存的数据
     * @return 缓存数据的对象
     */
    public <T> BoundSetOperations<String, T> setCacheSet(String key, Set<T> dataSet)
    {
        BoundSetOperations<String, T> setOperation = redisTemplate.boundSetOps(key);
        Iterator<T> it = dataSet.iterator();
        while (it.hasNext())
        {
            setOperation.add(it.next());
        }
        return setOperation;
    }

    /**
     * 获得缓存的set
     *
     * @param key
     * @return
     */
    public <T> Set<T> getCacheSet(String key)
    {
        Set<T> dataSet = new HashSet<T>();
        BoundSetOperations<String, T> operation = redisTemplate.boundSetOps(key);
        dataSet = operation.members();
        return dataSet;
    }

    /**
     * 缓存Map
     *
     * @param key
     * @param dataMap
     * @return
     */
    public <T> HashOperations<String, String, T> setCacheMap(String key, Map<String, T> dataMap)
    {
        HashOperations hashOperations = redisTemplate.opsForHash();
        if (null != dataMap)
        {
            for (Map.Entry<String, T> entry : dataMap.entrySet())
            {
                hashOperations.put(key, entry.getKey(), entry.getValue());
            }
        }
        return hashOperations;
    }

    /**
     * 获得缓存的Map
     *
     * @param key
     * @return
     */
    public <T> Map<String, T> getCacheMap(String key)
    {
        Map<String, T> map = redisTemplate.opsForHash().entries(key);
        return map;
    }

    /**
     * 获得缓存的基本对象列表
     *
     * @param pattern 字符串前缀
     * @return 对象列表
     */
    public Collection<String> keys(String pattern)
    {
        return redisTemplate.keys(pattern);
    }
}

5.4 Redis序列化类

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

import java.nio.charset.Charset;

public class FastJsonRedisSerializer<T> implements RedisSerializer<T> {

    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
    private Class<T> clazz;

    static
    {
        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
    }

    public FastJsonRedisSerializer(Class<T> clazz){
        super();
        this.clazz = clazz;
    }
    @Override
    public byte[] serialize(T t) throws SerializationException {
        if (t == null) {
            return new byte[0];
        }
        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
    }
    @Override
    public T deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length <= 0) {
            return null;
        }
        String str = new String(bytes, DEFAULT_CHARSET);
        return (T) JSON.parseObject(str, clazz);
    }
}

5.5 Redis配置类

import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    @SuppressWarnings(value = {"unchecked","rawtypes"})
    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate<Object, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        // 使用 FastJsonRedisSerializer 替换默认序列化
        FastJsonRedisSerializer fastJsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
        // 设置key和value的序列化规则
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(fastJsonRedisSerializer);
        // 设置hashKey和hashValue的序列化规则
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(fastJsonRedisSerializer);
        // 设置支持事物
        redisTemplate.setEnableTransactionSupport(true);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

原文作者:絷缘
作者邮箱:zhiyuanworkemail@163.com
原文地址:https://zhiyuandnc.github.io/fgzXm9vap/
版权声明:本文为博主原创文章,转载请注明原文链接作者信息