SpringSecurity



1. 简介

基于Spring框架,提供了一套Web应用完全性的完整解决方案。关于安全的两个主要区域:认证和授权

认证:就是系统认为用户是否能登录

授权:就是系统判断用户是否有权限去 做某些事情

特点:

  • 和Spring无缝整合
  • 全面的权限控制,功能多
  • 专门为Web开发而设计(旧版本不能脱离Web环境使用,新版本对整个框架进行了分层抽取)
  • 重量级框架(缺点)

同款的安全框架Shiro(Apache旗下的),特点:轻量级,通用性,灵活。缺点:再Web场景需要手动编写代码定制。

常见的技术栈组合:

SSM + Shiro

SpringBoot/SpringCloud + Spring Security


2. 入门介绍

2.1 使用

  1. 常见SpringBoot项目

    使用的是2.3.4.RELEASE版本的SpringBoot

  2. 引入相关依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    
  3. 编写Controller

    @Controller
    public class HelloController {
        
        @GetMapping("/hello")
        @ResponseBody
        public String hello() {
            return "Hello Spring Security";
        }
    }
    
  4. 访问 http://localhost:8080/hello 默认会重定向到 /login

    默认用户名是user,每次启动控制台会输出一个默认的密码,登陆后就能正常访问了

2.2 本质

SpringSecurity本质是一个过滤器链,过滤器在项目启动的时候就进行加载,下面看几个:

  • FilterSecurityInterceptor 是一个方法级别的权限过滤器,基于位于过滤器链的最底部
  • ExceptionTranslationFilter异常过滤器,用来处理认证授权过程中抛出的异常
  • UsernamePasswordAuthenticationFilter对/login的Post请求做拦截,校验表单中的用户名密码

AuthenticationManager,ProviderManger,AuthenticationProvider之间的关系⭐:

ProviderManger实现了AuthenticationManager接口(唯一实现),ProviderMangeer中有一个AuthenticationProvider的List集合,有多个,AuthenticationProvider可以有多个,建议我们去扩展AuthenticationProvider。

SpringSecurity支持多种不同的认证方式,如:用户名/密码认证,ReremberMe认证,手机验证码认证,每个不同的认证方式对应不同的AuthenticationProvider。底层会遍历所有的AuthenticationProvider去认证,只要有一个认证成功就登录成功。

ProviderManger也可也有多个,ProviderManger中可以有一个父亲ProviderManger,里面有通用的一系列认证规则,儿子ProviderManger可以有独特的认证规则。可以有多个儿子,但只能由一个父亲,这样设计便于后面的微服务。先使用本类ProviderManger中的所有AuthenticationProvider去认证,如果都认证不成功,就再去使用父亲ProviderManger中的所有AuthenticationProvider去认证。父亲相当于一种全局资源,作为所有提供者的后备资源。

如果后面我们想要扩展多种认证方式,最后修改全局的AuthenticationManager(ProviderManger),如果想要针对不同的资源进行不用的认证方式,即分的特别细则可以修改局部的(一般不会这么细)

全局的AuthenticationManager(ProviderManger)中有个AuthenticationProvider的实现叫DaoAuthenticationProvider,底层认证调用UserDetailService的实现类进行数据校验,最后根据返回的User在底层帮我们再进行密码的验证(因为会有一些加密)!

2.3 自动化配置

SpringBoot会自动化配置Spring Security:

SecurityFilterAutoConfiguration -> SecurityProperties -> spring.security配置项

里面注册的组件:

  • 帮我们配了:DelegatingFilterProxy -> DelegatingFilterProxyRegistrationBean

    这个Filter里面doFilter()会调用initDelegate(wac) 里面会从容器中获取id为filterChainProxy的Bean(FilterChainProxy类型)

  • 调用FilterChainProxy.doFilterInternal() 方法:

    在里面获取所有Filter,List<Filter> filters = getFilters(..) ,即获取Security的过滤器链(十几个过滤器)进行加载到过滤链中

2.4 三个重要接口

如果要自定义验证用户名密码则需要继承UsernamePasswordAuthenticationFilter 类,重写attemptAuthentication()方法得到自己的用户名密码,成功重写successfulAuthentication()方法,失败重写unsuccessfulAuthentication()方法

  1. UserDetailsService

    这个实现类是查询数据库用户名和密码的过程,返回User对象(是安全框架中提供的对象)

  2. PasswordEncoder

进行密码加密,用于User对象里面的密码加密

  1. WebSecurityConfigurerAdapter 自定义 SpringSecurity

3. 设置用户名密码

3.1 内存实现

三种方法:

  1. 配置文件

    spring.security.user.name=admin
    spring.security.user.password=123
    
  2. 通过配置类

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            String password = encoder.encode("123"); // 加密
            auth.inMemoryAuthentication().passwordEncoder(encoder)
                .withUser("admin").password(password).roles("管理员");
        }
    }
    
  3. 自定义编写实现类⭐

    • 编写UserDetailsService实现类,配置类中配置
    • 再实现类在查数据库,返回User对象
    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
        }
    }
    
    @Service("userDetailsService")
    public class MyUserDetailsService implements UserDetailsService {
        @Override
        public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
            // 查数据得到用户名密码以及操作权限,封装User返回 (User implements UserDetails)
    
            List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
            return new User("admin", new BCryptPasswordEncoder().encode("123"), auths);
        }
    }
    

    BCryptPasswordEncoder可以把这个对象放到容器,就不用每次new了

3.2 数据库实现

使用MybatisPlus(引入mysql和MyBatisPlus依赖)

  1. 创建表t_user,有字段id,username,password。然后配置数据源!

  2. 创建实体类MyUser(防止和Security的User冲突)

    @TableName("t_user")
    public class MyUser {
        private Integer id;
        private String username;
        private String password;
    }
    
  3. 编写Mapper和Service (认证)

    public interface MyUserMapper extends BaseMapper<MyUser> { }
    
     
    @Service
    public class MyUserDetailsService implements UserDetailsService {
        @Autowired
        private MyUserMapper userMapper;
    
        @Override
        public UserDetails loadUserByUsername(String username) 
            throws UsernameNotFoundException {
            // 根据用户名去数据库查询
            LambdaQueryWrapper<MyUser> wrapper = new LambdaQueryWrapper<>();
            wrapper.eq(MyUser::getUsername, username);
            MyUser user = userMapper.selectOne(wrapper);
            if (user == null) {  // 认证失败,直接抛异常!!
                throw new UsernameNotFoundException("用户不存在"); 
            }
            // 这里也可以数据库建立一个关系表,根据用户id查询出来对应的角色!
            List<GrantedAuthority> auths =
                AuthorityUtils.commaSeparatedStringToAuthorityList("role");
            return new User(user.getUsername(), 
                            new BCryptPasswordEncoder().encode(user.getPassword()),
                            auths);
        }
    }
    
  4. MyUserDetailsService配置上,并且开启Mapper接口扫描@MapperSacn

    @Configuration
    @MapperScan("com.sutong.mapper")
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private MyUserDetailsService myUserDetailsService;
    
        // 自定义全局的AuthenticationManager(ProviderMange)
        @Override
        protected void configure(AuthenticationManagerBuilder builder) throws Exception {
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            // 覆盖掉底层默认的的InMemoryUserDetailsService() 
            builder.userDetailsService(myUserDetailsService).passwordEncoder(encoder);
        }
    }
    

3.3 认证信息获取

SpringSecurity会将用户认证信息也会保存到Session中,但为了方便,进行了线程绑定(默认使用ThreadLocal实现的),将用户信息保存到SecurityContextHolder中。

每当有请求发来,会先从Session取出用户信息,保存到SecurityContextHolder中,方便后续处理使用,每当请求结束后会将SecurityContextHolder数据保存到Session,同时将SecurityContextHolder中数据清空!!

⭐实际上SecurityContextHolder中存储的是SecurityContext,SecurityContext中存有Authentication (认证后的用户信息)

后端使用:

@RequestMapping("/getUser")
@ResponseBody
public String getUser() {
    Authentication au = SecurityContextHolder.getContext().getAuthentication();//默认子线程取不到
    User user = (User) au.getPrincipal(); 
    System.out.println("username:" + user.getUsername());  // 密码拿不到
    System.out.println("权限和角色信息:" + au.getAuthorities());
    return "success"; // 如果前后端分离直接返回json数据就行了
}

MODE_THREADLOCAL(只能当前线程访问) / MODE_INHERTABLETHREADLOCAL (当前和子线程) ,需改需要在加JVM参数spring.security.strategy=… ,不是简单的在配置文件中配置。

页面中怎么获取??引入Thymeleaf的扩展,可以更好的操作使用SecurityContextHolder

官网:thymeleaf-extras-springsecurit(前后端分离的话更简单了,就不用这个,直接获取json数据就行了)

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>

使用(authentication认证,authorize授权):

<html xmlns:sec="http://www.thymeleaf.org/extras/spring-security"> <!-- 命名空间-->

<div sec:authentication="name"></div>   <div th:text="${#authentication.name}"></div>  <!--用户名-->
<div th:if="${#authentication.authenticated}">已经登录</div> 
<ul>

 <li sec:authentication="principal.username"></li>    <!-- 用户名,principal是当前用户对象-->
 <li sec:authentication="principal.authorities"></li> <!-- 权限和角色-->
 <li sec:authentication="principal.accountNonExpired"></li> <!-- 是否没过期-->
 <li sec:authentication="principal.accountNonLocked"></li>  <!-- 是否没锁定-->
 <li sec:authentication="principal.credentialsNonExpired"></li> <!-- 凭证是否没过期-->
</ul>

<div sec:authorize="hasRole('ROOT')">是ROOT</div>
<div sec:authorize="isAuthenticated()">已经登录</div>
<div th:if="${#authorization.expression('hasRole(''ROLE_ADMIN'')')}">
	仅当经过身份验证的用户具有角色ROLE_ADMIN时,才会显示此内容。
</div>

4. 自定义登录页面

4.1 配置

引入Thymeleaf 模板引擎

  1. 去配置类中配置⭐

    @Configuration
    @MapperScan("com.sutong.mapper")
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        
        // 配置登录页(我们想要/hello保护,/index和/login.html不用认证)
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests() // 可以定义哪些被保护哪些不被保护
                 .mvcMatchers("/login.html", "/index").permitAll() // 设置放行的资源,需要放到前面
                 .anyRequest().authenticated() // 除了上面的请求都需要登录
                 .and()
                 .formLogin() // 自定义自己编写的登录页面
                 .loginPage("/login.html") // 覆盖默认的登录页面,(这个是login.html请求!不是直接访问)
                 .loginProcessingUrl("/doLogin")  // 默认是/login,一旦自定义页面后必须指定上!!
                 .and()
                 .csrf().disable();  // 关闭csrf跨站请求保护
        }
    }
    
    
    /*  .loginProcessingUrl("/doLogin")  指定登录处理的url!!
        登录访问路径(表单提交后提交到那个Controller,不用我们写,SpringSecurity帮我们)
        (如果是前后端分离的话底层的getParameter就不行了,需要我们继承UsernamePasswordAuthenticationFilter,
        去重写attemptAuthentication方法,在配置类中注入到容器,需要设置一些属性,例如AuthenticationManager..
        把json数据解析,提取username,password,再进行认证!!)
        我们要替换这个Filter,而且执行顺序不能改变,可以这样:
        // before..放在过滤器链中那个filter之前
        // after..放在过滤器链中那个filter之后
        // at..用某个filter替换过滤器链那个filter!
        http.addFilterAt(MyUsernamePasswordAuthenticationFilter(),
        					UsernamePasswordAuthenticationFilter.class);   
            
        .usernameParameter("uname")可以改下面表单中input的name值。  .passwordParameter("passwd")
    */
    
  2. 创建登录页面classpath:templates/login.html

    <body>
        <!--①必须是post请求,②action必须和上面的loginProcessingUrl参数一致,这个请求SpringSecurity帮我们处理
    		③表单中的两个input的name必须是下面的!username,password,-->
        <form th:action="@{/doLogin}" method="post">
            用户名:<input type="text" name="username"> <br>
            密码:<input type="password" name="password"> <br>
            <input type="submit" value="登录">
        </form>
    </body>
    
  3. 创建Controller

    @Controller
    public class HelloController {
        @RequestMapping("/login.html")
        public String login() {
            return "login"; // 跳转到 templates/login.html
        }
        
        @RequestMapping("/index")
        public String index() {
            return "index";
        }
        
        @GetMapping("/hello")
        @ResponseBody
        public String hello() {
            return "Hello Spring Security";
        }
    }
    

    web技巧:

    public class WebConfig implements WebMvcConfigurer {
           // 这个就不用每个请求/login.html -> templates/login.html去写个控制器方法进行控制跳转了
           @Override
           public void addViewControllers(ViewControllerRegistry registry) {
               //registry.addRedirectViewController(); 重定向跳转
               registry.addViewController("/login.html").setViewName("login"); // 转发跳转,自动加前后缀
               registry.addViewController("/index").setViewName("index"); 
           }
    }
    
  4. 测试

    访问http://localhost:8080/index 不需要登录

    http://localhost:8080/hello 则需要到我们自定义页面登录,如果使用defaultSuccessUrl登陆后会继续访问我们的/hello请求

4.2 登录成功后处理

.successForwardUrl("..") 登录成功后forward转发的路径(这个不管之前有没有请求,都去这个)

.defaultSuccessUrl("..") 登录成功后redirect转发的路径(如果保存了登录前的路径就会优先跳转保存的),这个还有个第二个参数,如果设为true和successForwardUrl就效果一样了!这两个只能选一个

successHandler(..) 前后端分离的认证成功后的处理(认证成功后的处理,上面两个并不适用于前后端分离,我们想要返回json数据不需要跳转,通知是否认证成功,自定义类实现AuthenticationSuccessHandler,然后传入):

public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
        Map<String, Object> res = new HashMap<>();
        res.put("msg", "登陆成功");
        res.put("status", 200);
        res.put("authentication",authentication); // 认证后的信息都在authentication里面
        httpServletResponse.setContentType("application/json;charset=utf-8");
        String jsonRes = new ObjectMapper().writeValueAsString(res); // 转化为json
        httpServletResponse.getWriter().write(jsonRes);
    }
}

4.3 登录失败后处理

如果认证失败后是forward跳转会把异常信息放入request域中

redirect则会放入session域中(默认是使用重定向跳转的),key都是SPRING_SECURITY_LAST_EXCEPTION

配置类中的方法 :

.failureForwardUrl("..") forward跳转

.failureUrl("..") redirect跳转,一般用这个

.failureHandler(..) 自定义类实现AuthenticationFailureHandler传入,和上面的successHandler(..)一样,便于前后端分离

public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        Map<String, Object> res = new HashMap<>();
        res.put("msg", "登陆失败" + e.getMessage());
        res.put("status", 500);
        String jsonRes = new ObjectMapper().writeValueAsString(res); // 转化为json
        httpServletResponse.getWriter().write(jsonRes);
    }
}

5. 授权Authorization

主体:Principal

认证 Authentication

授权 Authorization

授权:访问控制,控制谁能访问哪些资源,可以基于权限或角色进行访问控制!

// 用户信息对象
public interface Authentication extends Principal, Serializable {
    Collection<? extends GrantedAuthority> getAuthorities(); // 获得当前登录用户具备的权限信息
    
    //......
}

GrantedAuthority :如果是基于资源进行权限管理,则是权限字符串,如果基于资源进行权限管理,则是角色,如果两者都有则统称为权限。

从设计层面考虑,角色和权限是两个完全不同的东西,权限是一些具体操作,角色的某些权限的集合。

从代码方面没有太大不同,处理方式基本一样,唯一区别SpringSecurity会在角色前自动加ROLE_前缀,权限则不会。

权限管理策略:

  • 基于过滤器/URL的权限管理(FilterSercurityInterceptor)

    基于Filter,拦截HTTP请求,根据HTTP请求地址进行权限校验

  • 基于方法/Aop的权限管理(MethodSecurityInterceptor)

    处理方法级别的权限问题,通过AOP将操作拦截下来,进行判断

5.1 基于URL的权限管理

基于资源进行权限管理:

  1. hasAuthority() 如果当前的主体具有指定的权限,则返回 true,否则返回 false

    // 配置类加上,标识当前登录的用户具有admins的权限才能访问前面的路径
    .mvcMatchers("/getInfo").hasAuthority("read_info")  
    

    在UserDetailsService的实现类中,返回的User中设置权限(当然这个权限要根据业务进行添加删除)

    List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("read_info");
    return new User(user.getUsername(), new BCryptPasswordEncoder().encode(user.getPassword()), auths);
    // 如果没有权限则会报错 (type=Forbidden, status=403),这个报错页面我们也可以自定义
    
  2. hasAnyAuthority() 多个权限,如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回 true.

    .mvcMatchers("/getInfo").hasAnyAuthority("read_info,write_info")
    

基于角色进行权限管理:

  1. hasRole() 如果用户具备给定角色就允许访问,否则出现 403。 如果当前主体具有指定的角色,则返回 true。

    // 这里不用加前缀,hasRole方法里面会自动帮我们加上
    .mvcMatchers("/protection").hasRole("employee") 
        
    // 在添加角色使和上面不太一样,要加个ROLE_前缀!!!
    // 如果将来要在数据库查询角色,数据库中不会存储ROLE_前缀的,需要我们手动加上
    List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_sale");
    
  2. hasAnyRole() 表示用户具备任何一个角色都可以访问。

    .mvcMatchers("/protection").hasAnyRole("employee,boss") 
    

antMatchers(),最早出现的,用法和后者一样

mvcMatchers(),使用SpringMVC用于匹配的相同规则,例如"/path"映射会匹配"/path",“/path/”,"/path.html"等,匹配不到会交给ant模式

regexMatchers(),支持正则表达式

5.2 基于方法的权限管理

MethodSecurityInterceptor 除了可以前置处理外还可以进行后置处理,即是一个环绕AOP。

开启权限注解功能 :

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
}

// securedEnabled 开启 @Secured
// prePostEnabled 开启 @PostAuthorize @PostFilter @PreAuthorize @PreFilter 只有这四个支持权限表达式!!
// jsr250Enabled 开启 JSR-250提供的注解 @DenyAll @PermitAll @RolesAllowed
  1. @Secured 判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。

    在Controller的方法上使用注解,设置角色

    @Secured({"ROLE_employee", "ROLE_boss"}) // 或的关系
    @GetMapping("/protection")
    @ResponseBody
    public String protection() {
        return "test secured";
    }
    
  2. @PreAuthorize 进入控制器方法执行前的权限验证

    可以将登录用户的 roles/permissions 参数传到方法中,下面这四个都支持spEL。⭐

    // PreAuthorize注解里面写四个方法都行,即权限表达式!
    // 当前登录用户角色是ADMIN,用户名是root才可以访问 /protection
    @PreAuthorize("hasRole('ADMIN') and authentication.name=='root'")  
    @GetMapping("/protection1")
    public String protection1() {
        return "test preAuthorize1";
    }
    
    
    // 当前登录的用户名和参数name的值对比认证,#name取的是方法的参数
    @PreAuthorize("authentication.name==#name")  
    @GetMapping("/protection2")
    public String protection2(String name) {
        return "test preAuthorize2";
    }
    
  3. PostAuthorize 在控制器方法执行后再进行权限验证,适合验证带有返回值的权限

    控制器方法使用:

    // returnObject表示方法的返回值。可在注解里面使用
    @PostAuthorize("hasRole('sale')")    
    @GetMapping("/protection")
    public String protection() {
        System.out.println("haha"); //如果没有sale角色也能输出haha,校验在方法执行后(输出后跳转到没有访问权限页面)
        return "a protection";
    }
    
    
    // 返回对象的id是1才能返回
    @PostAuthorize("returnObject.id==1")   
    @GetMapping("/testPostAuthorize")
    public MyUser testPostAuthorize() {
        return new MyUser(1, "su", "123");
    }
    
  4. @PreFilter 进入控制器之前对集合/数组类型的参数进行过滤(下面这两个用的不多)

    filterObject 是使用@PreFilter和@PostFilter时的一个内置表达式,表示集合中的当前对象。

    // value过滤规则,filterTarget要过滤的参数名(只有一个参数可以省略)
    @PreFilter(value = "filterObject.id%2==0", filterTarget = "list")
    @PostMapping("/testPreFilter")
    public void testPreFilter(@RequestBody List<MyUser> list) {
        list.forEach(t -> {
            System.out.println(t.getId() + "\t" + t.getUsername());
        });
    }
    
  5. @PostFilter 权限验证之后对集合/数组类型的返回值进行过滤(留下用户名是 admin1 的数据)

    @PostFilter("filterObject.username=='admin1'")
    @RequestMapping("/getAll")
    public List<MyUser> getAllUser(){
        ArrayList<MyUser> list = new ArrayList<>();
        list.add(new MyUser(1L,"admin1","6666"));
        list.add(new MyUser(2L,"admin2","8888"));
        return list; // 返回前端就只有admin1的数据
    }
    

5.3 源码分析

三个接口 AccessDecisionManager , AccessDecisionVoter, ConfigAttribute

处理授权的主要的过滤器是:FilterSecurityInterceptor

ConfigAttribute:用户每请求一个资源需要的角色会被封装成一个ConfigAttribute对象,在ConfigAttribute中只有一个getAttribute方法,获得角色的名称(String),一般都带有ROLE_前缀

AccessDecisionVoter:投票器,其实就是比较用户所具有的角色和请求某个资源所需的ConfigAttribute之间的关系

AccessDecisionManager:在该类中会挨个遍历AccessDecisionVoter,进而决定是否允许用户访问

动态的权限管理:

实际应用开发中,权限管理并不是硬编码到配置类中,而是需要存储到数据库,来实现动态的url权限管理,需要实现接口FilterInvocationSecurityMetadataSource,实现getAttribute方法

表结构:用户 <-- 多对多,用户角色表–> 角色 <-- 多对多,角色菜单表–> 菜单

具体操作看不良人的视频!

5.4 自定义403页面

自定义页面 templates/unauth.html

<body>
    <h1>没有访问权限</h1>
</body>

配置类:

// 配置403页面
@Override
protected void configure(HttpSecurity http) throws Exception {
    http.mvcMatchers("/protection").hasAuthority("admins")  
    http.exceptionHandling().accessDeniedPage("/unauth.html"); // 这行
}

Controller:

@RequestMapping("/unauth.html")
public String unauth() {
    return "unauth"; // 跳转到 templates/unauth.html
}

6. 用户注销

  1. 配置类中配置

    http.logout().logoutUrl("/logout").logoutSuccessUrl("/index").permitAll(); // 这行都可以不写
    
    // logoutUrl("/logout")这个不加也行,默认的/logout(get方式),(我们可以自定义Controller处理这个,例如/doLogout)
    

    Ⅰ配置多个注销登录请求:

    // 注销
    http.logout().logoutRequestMatcher(new OrRequestMatcher(   // 配置多个注销登录请求,还可以修改请求方式!
           new AntPathRequestMatcher("/login", "GET"),
           new AntPathRequestMatcher("/login02", "POST")
    )).logoutSuccessUrl("/index").permitAll();
    

    注销的前后端分离方案(不需要页面跳转,这需要返回注销成功的信息,自定义LogoutSuccessHandler):

    LogoutSuccessHandler的实现类这里不举例,和上面的登录成功的handler类似。

    http.logout().logoutUrl("/logout").logoutSuccessHandler(new MyLogoutSuccessHandler()).permitAll();
    
  2. index.html里面创建一个注销超链接

    <body>
        <h1>欢迎</h1>
        <a th:href="@{/logout}">注销</a>
    </body>
    

    点击超链接再次访问受保护资源就再次需要登录了,这个/logout请求SpringSecurity帮我们解决的,是LogoutFilter处理的!!


7. 验证码

传统web步骤:

  1. 在表单添加验证码输入框
  2. 创建生成验证码的Controller
  3. 重写UsernamePasswordAuthenticationFilter,认证前要先进行比较验证码
  1. 依赖

    <dependency>
        <groupId>com.github.penggle</groupId>
        <artifactId>kaptcha</artifactId>
        <version>2.3.2</version>
    </dependency>
    
  2. 生成验证码的配置

    @Configuration
    public class KaptchaConfig {
        // 设置验证码的生成规则
        @Bean
        public Producer kaptcha() {
            Properties prop = new Properties();
            prop.setProperty("kaptcha.image.width", "150"); // 验证码的宽度
            prop.setProperty("kaptcha.image.height", "30"); // 高度
            prop.setProperty("kaptcha.textproducer.char.string", "0123456789"); // 验证码字符串
            prop.setProperty("kaptcha.textproducer.char.length", "4"); // 验证码长度,还有很多配置..
            Config config = new Config(prop);
            DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
            defaultKaptcha.setConfig(config);
            return defaultKaptcha;
        }
    }
    
  3. Controller(或者直接用KaptchaServlet)

    @Controller
    public class VerifyCodeController {
        // 注入能生成验证码的类!
        @Autowired
        private Producer producer;
    
        // 发请求,响应一个验证码图片!
        @RequestMapping("/vc.jpg")
        public void verifyCode(HttpServletResponse resp, HttpSession session) throws IOException {
            // 1.生成验证码,保存到session(后面认证需要比较)
            String verifyCode = producer.createText();
            session.setAttribute("kaptcha", verifyCode);
            // 2.生成验证码图片
            BufferedImage bi = producer.createImage(verifyCode);
            // 3.响应图片(记得设置响应类型)
            resp.setContentType(MediaType.IMAGE_PNG_VALUE); // "image/png"
            ServletOutputStream os = resp.getOutputStream();
            ImageIO.write(bi, "jpg", os); // ImageIO是Java提供的工具类,中间的参数是以什么格式
            
            // 如果是前后端分离的话就不能使用响应流进行响应图片了,要使用Base64编码,这个方法返回值就需要是String了
            //FastByteArrayOutputStream fos = new FastByteArrayOutputStream();
            //ImageIO.write(bi, "jpg", fos);
            //return Base64Utils.encodeToString(fos.toByteArray()); 
        }
    }
    
  4. 把生成验证码的url放行,即不需要认证

    .mvcMatchers("/login.html", "index", "/vc.jpg").permitAll()
    
  5. 表单中添加验证码输入框,这时访问就能看到图片了

    <form th:action="@{/doLogin}" method="post">
        用户名:<input type="text" name="username"> <br>
        密码:<input type="password" name="password"> <br>
        验证码:<input type="text" name="kaptcha"> <img th:src="@{/vc.jpg}"> <br>
        <input type="submit" value="登录">
    </form>
    
  6. 重写Filter替换UsernamePasswordAuthenticationFilter

    参考UsernamePasswordAuthenticationFilter里面的写法!!

    // 自定义验证码的filter
    public class KaptchaFilter extends UsernamePasswordAuthenticationFilter {
        public static final String FROM_KAPTCHA_KEY = "kaptcha"; 
        private String kaptchaParameter = FROM_KAPTCHA_KEY;  // 验证码的key,不在代码中写死,将来可以set
    
        @Override
        public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
            // 1.判断是否是POST请求
            if (!request.getMethod().equals("POST")) {
                throw new AuthenticationServiceException(
                    "Authentication method not supported: "+ request.getMethod());
            }
            // 2.获取验证码(如果是前后端分离的话,从json中提取出来验证码,username,password,不能直接getParameter了)
            String verifyCode = request.getParameter(getKaptchaParameter());
            String sessionVerifyCode = (String) request.getSession().getAttribute("kaptcha"); 
            if (verifyCode != null && sessionVerifyCode != null 
                && verifyCode.equalsIgnoreCase(sessionVerifyCode)) {
                return super.attemptAuthentication(request, response);
            }
            // 3.验证码不正确则抛异常,是自定义的验证码异常
            throw new KaptchaNotMatchException("验证码不匹配");
        }
    
        
        public String getKaptchaParameter() {
            return kaptchaParameter;
        }
        public void setKaptchaParameter(String kaptchaParameter) {
            this.kaptchaParameter = kaptchaParameter;
        }
    }
    
  7. 指定我们自定义的filter替换过滤器链中的哪个个Filter ⭐

    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private MyUserDetailsService myUserDetailsService;
        @Autowired
        private DataSource dataSource;
        
        // 自定义AuthenticationManager,从数据库查询User
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
            auth.userDetailsService(myUserDetailsService).passwordEncoder(encoder);
        }
        
        // 上面这样自定义AuthenticationManager我们并不能直接注入,需要进行暴露本地的Bean,重写这个方法进行 + @Bean
        @Bean 
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
        
        // 配置我们自定义的Filter,注入容器
        @Bean
        public KaptchaFilter kaptchaFilter() throws Exception {
            KaptchaFilter filter = new KaptchaFilter();
            // 这个可以自定义表单里面的name,有默认值 (setPasswordParameter也行)
            filter.setKaptchaParameter("kaptcha"); 
            filter.setFilterProcessesUrl("/doLogin");
            filter.setAuthenticationManager(authenticationManagerBean()); // 一旦自定义就要指定认证管理器
            
            // 认证成功和失败后的处理:
            filter.setAuthenticationSuccessHandler((request, response, authentication) -> {
                // 如果是前后端分离的话,响应对应的json数据就行
                response.sendRedirect("/index");
            });
            filter.setAuthenticationFailureHandler((request, response, e) -> {
                // 自定义,则需要自己存异常信息了
                response.sendRedirect("/login.html");
            });
            return filter;
        }
    
    	// 配置需要认证url,认证页面,替换管理器 (还可以配置异常页面,注销,rememberMe...)
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests() 
                .mvcMatchers("/login.html", "index", "/vc.jpg").permitAll()
                .anyRequest().authenticated() 
                .and()
                .formLogin() 
                .loginPage("/login.html")  // 自定义的话,登录成功登录失败跳转的url就不能在这里写了
                .and()
                .csrf().disable(); 
            
            http.addFilterAt(kaptchaFilter(), UsernamePasswordAuthenticationFilter.class);
                               
            http.exceptionHandling()
                .accessDeniedPage("/unauth.html");  // 授权异常处理,还有accessDeniedHandler
            	//.authenticationEntryPoint();  
            	//认证异常处理,如果是前后端分离的话,没有认证时,可以自定义返回json,而不是去login.html
        }
    }
    

8. 记住我

以前我们在Web阶段使用Cookie和Session相关技术实现过,下面我们使用数据库来实现:

数据库版本SpringSecurity帮我们封装了,简单了很多!

原理:

  1. 浏览器 <- (通过cookie存一个加密串) 认证成功 (存cookie的加密串,用户对应信息字符串)-> 数据库

  2. 再次访问,获取cookie信息(7,10…天有效),拿着cookie信息到数据库比对,如果查到对应信息则认证成功,登录

  1. 建表 (其实在JdbcTokenRepositoryImpl可以帮我们自动创建表)

    CREATE TABLE `persistent_logins`(
        `username` varchar(64) NOT NULL,
        `series` varchar(64) NOT NULL,
        `token` varchar(64) NOT NULL,
        `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 
        CURRENT_TIMESTAMP,
        PRIMARY KEY (`series`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
  2. 配置类:配置操作数据库的对象JdbcTokenRepositoryImpl和配置免登录的参数信息!!

    @Configuration
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private MyUserDetailsService myUserDetailsService;
        @Autowired
        private DataSource dataSource;  //我们没配置,使用的默认的数据源HikariDataSource
        
        @Bean
        public PersistentTokenRepository persistentTokenRepository() {
            JdbcTokenRepositoryImpl repository = new JdbcTokenRepositoryImpl();
            repository.setDataSource(dataSource);
            //repository.setCreateTableOnStartup(true); 这个会帮我们创建表,第一次的时候设置true就行
            return repository;
        }
        
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 免登录配置
            http.rememberMe().tokenRepository(persistentTokenRepository());  // 配置操作数据库的对象
                
           	// tokenValiditySeconds(..) 有效时长,秒为单位
            // userDetailsService(..)   设置UserDetailsService
        }
    }
    
  3. 在登录页面添加复选框,是否下次免登录

    <form th:action="@{/doLogin}" method="post">
        用户名:<input type="text" name="username"> <br>
        密码:<input type="password" name="password"> <br>
        记住我:<input type="checkbox" name="remember-me" value="true"> <br> 
        <!-- name默认必须是remember-me!当然可以在配置类中改参数名!value的可选值true,yes,1,on-->
        <input type="submit" value="登录">
    </form>
    
  4. 登陆时勾选上自动登录,关闭浏览器再次访问就不用再次登录了(数据库也出现信息了!!)

前后端分离场景:

  • 拓展UsernamePasswordAuthentticationFilter,重写attemptAuthentication()方法(具体写法看上面的验证码的KaptchaFilter类似),解析json中的username,password,remember-me数据(把remember-me参数存到request域中下面会用到)

  • 扩展PersistentTokenBasedRememberMeServices,重写继承父类的rememberMeRequested()方法,判断remember-me参数值是不是true|yes|1|on,是则返回true

  • 把我们扩展的PersistentTokenBasedRememberMeServices类,配置到容器中,并且在开启remeber的配置后面指定我们的自定义的类,即指定使用我们拓展的rememberMeServices。(注意:我们拓展的UsernamePasswordAuthentticationFilter,配置时也要设置一下rememberMeServices,因为登陆成功后要使用这个Services,往客户端写Cookie,而前面的设置的Services是在会话过期后进行比对的Cookie的)

    @Bean
    public MyPersistentTokenBasedRememberMeServices rememberMeServices() {
        return new MyPersistentTokenBasedRememberMeServices(UUID.randomUUID().toString(),
                                                            userDetailsService(),
                                                            persistentTokenRepository());
    }
                                  
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.rememberMe().rememberMeServices(rememberMeServices());
    }
    

9. 密码加密

常见加密:

  • Hash算法

    单向的,相同的明文加密的字符串相同,可以手动加"盐"

  • 单向自适应函数(SpringSecurity推荐)

    • bcrypt(推荐)故意降低运行速度,为自己带"盐",即使相同的明文加密字符串都不相同

    • PBKDF2 故意降低运行速度,当需要FIPS认证时是个好的选择(美国用的多)

    • scrypt,argon2…

  • {noop}123 明文

  • {bcrypt}加密后的 bcrypt算法

  • {pbkdf2}加密后的 bcrypt算法

  • {MD5}加密后的 MD5算法

SpringSecurity5后默认的加密方式并不是上面的某一种,而是DelegatingPasswordEncoder,这个不是一个具体的加密方式,这个默认的PasswordEncoder自动根据前缀,使用对应具体的xxxPasswordEncoder,进行对数据库的密码解密!和用户输入的进行比对!

这样的设计好处:兼容性,便携性…

加密两种方式:

  1. 加前缀

    {bcrypt}$2a$10$gU4BAdZX/a4pGtHcdJ4wIexSV39f5oK/CeYkREOBg3FMFrf71LPse

  2. 固定系统的加密方案

    默认创建的是DelegatingPasswordEncoder,我们要修改这个,我们只要在容器中创建一个具体的xxxPasswordEncoder,默认的就不会使用了,不需要加前缀了

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

10. 会话管理

当浏览器调用登录接口登录成功后,服务器和浏览器之间会建立一个会话(Session),在每次发送请求时都会携带一个SessionId,服务器会根据这个SessionId来判断用户身份。当浏览器关闭后,服务器的Session并不会自动销毁,需要在服务器端调用销毁方法,或者等Session过期自动销毁(Tomcat中默认30分钟)

SpringSecurity中,与HttpSession相关的功能由SessionManagemenFilter过滤器和SessionAuthenticationStrategy接口来处理,过滤器将Session相关操作委托给接口去完成。

10.1 会话并发管理

是指当前系统中同一个用户可以同时创建多少个会话(可以理解为:同一个用户,在同一时间可以在多少个客户端登录),默认没有限制,可以在SpringSecurity中对此进行配置。

http.sessionManagement()          // 开启会话关闭
    .maximumSessions(1)           // 允许会话最大并发一个客户端
    .expiredUrl("/login.html");   // 超过最大限制,默认则会有会话被挤掉。传统web开发被挤下线后的处理

前后端分离下,被挤下线的的处理:

http.sessionManagement()                
        .maximumSessions(1)
        // 前后端分离的处理(可以写个类实现SessionInformationExpiredStrategy接口)
        .expiredSessionStrategy(event -> { 
            HttpServletResponse response = event.getResponse();
            HashMap<String, Object> res = new HashMap<>();
            res.put("status", 500);  res.put("msg", "当前会话已经过期,请重新登录");
            String s = new ObjectMapper().writeValueAsString(res);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().println(s);
            response.flushBuffer();
        });

另一种机制,禁止后来者登录,即一旦当前用户登陆成功,后来者无法再次使用相同的用户登录,直到当前用户主动注销登录。

http.sessionManagement()                 // 开启会话关闭
        .maximumSessions(1)              // 允许会话最大并发一个客户端
        .maxSessionsPreventsLogin(true); // 登录之后禁止再次登录

10.2 会话共享

前面所讲的会话管理都是单机上的会话管理,如果当前是集群环境,前面所讲的会话管理方案会失效,此时可以使用spring-session结合redis实现session共享。

依赖:

<!-- 操作redirs的(记得配置连接redis)-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 序列化session的-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-session-data-redis</artifactId>
</dependency>

配置:

public class SecurityConfig extends WebSecurityConfigurerAdapter {
    // 操作Session的对象
    @Autowired
    private final FindByIndexNameSessionRepository sessionRepository;
    
    // 创建将Session同步到redis中的方案
    @Bean
    public SpringSessionBackedSessionRegistry sessionRegistry() {
        return new SpringSessionBackedSessionRegistry(sessionRepository);
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests() 
            .anyRequest().authenticated() 
            .and()
            .formLogin() 
            .and()
            .csrf().disable()
        	.sessionManagement()              	  // 开启会话关闭
            .maximumSessions(1)            		  // 允许会话最大并发一个客户端
            .maxSessionsPreventsLogin(true); 	  // 登录之后禁止再次登录
        	.sessionRegistry(sessionRegistry())   // 共享会话
    }
}

11. CSRF漏洞保护

跨站请求伪造(Cross-site request forgery),是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。

简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个 自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。 这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。

Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用 程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。

11.1 令牌同步模式

CSRF防御:在合法请求中额外携带一个攻击者无法获取的参数,就可以区分两种不同的请求,进而直接拒接恶意请求,称令牌同步模式。具体操作是,在每个HTTP请求中,除了默认自动携带的Cookie参数外,在提供一个安全的随机的字符串(CSRF令牌),这个CSRF令牌是由服务器端生成,生成后在HttpSession中保存一份。当前端请求到达后,将携带的CSRF令牌信息和服务器中保存的进行对比,不相等则拒接掉该请求。(注意**:每一个请求都会生成一个CSRF令牌,而且每次请求令牌都会重新生成,默认保存到Session作用域**)

CSRF令牌应该在服务器端生成。它们能够针对每个用户会话或个请求生成一次。每个请求一个令牌比每个会话一个要安全点,因为这样留给攻击者利用偷来的令牌的时间最少。

开启:

SpringSecurity 中开启csrf防御: http.csrf() (默认也是开启的)

幂等性的请求(GET,HEAD,TRACE,OPTIONS)可以不携带CSRF令牌

11.2 传统web开发

开启后,如果页面经解析后发现有表单的话,会自动在提交的表单中加入如下代码(无需做任何配置)

如果不能自动加入的话,需要开启之后手动加上如下的代码,并随着请求提交。

<input type="hidden" 
       th:if="${_csrf}!=null" 
       th:name="${_csrf.parameterName}"
       th:value="${_csrf.token}"
/>

11.3 前后端分离

CSRF令牌不在由服务器端进行保存到Session作用域中,而是浏览器本身进行保存,即保存到Cookie中,即使恶意网站携带这个Cookie也不会请求成功的。因为服务器进行验证的话不仅看Cookie中是否有CSRF令牌,还得看请求头中是否有key=CSRF令牌,而恶意网站不知道怎么组装这个key=value

// 告诉SpringSecurity令牌存储的机制,即将CSRF令牌保存到Cookie中,key是CSRF-TOKEN,并允许前端获取
http.csrf().csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); 

发请求 (两种)

  • 请求参数携带令牌,_csrf参数

    {
    	"username": "root",
    	"passward": "123",
    	"_csrf": "2c184260-157d-4323-a087-ad962b8c251d" (这个令牌前端需要在Cookie中获取,key是CSRF-TOKEN}
    
  • 请求头中携带令牌,X-CSRF-TOKEN参数

    "X-CSRF-TOKEN""2c184260-157d-4323-a087-ad962b8c251d"
    

    前后端分离我们更喜欢后者,即请求头携带,这样后端就不用重写UsernamePasswordAuthenticationFilter,去解析json数据获取去"_csrf"参数了。


12. CORS跨域

Spring 框架中对于跨域问题的处理方案有好几种

12.1 CORS简介

CORS(Cross-Origin Resource Sharing),由W3W指定的一种资源共享技术标准,即在请求头中添加一些字段,服务器告诉浏览器,哪些网站通过浏览器有权限访问哪些资源,同时规定,对于哪些可能修改服务器数据的HTTP请求方法(GET以外的),浏览器必须使用OPTIONS方法发起一个预检请求,如果服务器允许才发生实际的HTTP请求。

同源:协议+主机+端口 相同,同源策略CORS

简单请求:

GET请求为例子,发生一个简单的跨域请求,请求头如下:

Host: localhost:8080  							# 要访问的服务器
Origin: http://localhost:8081 					# 本服务器
Referer: http://localhost:8081/index.html

如果服务器支持该跨域请求,那么返回的响应头中应包含如下字段:

# Access-Control-Allow-Origin 字段来告诉浏览器访问资源的域
# 浏览器解析后发现该值包含当前页面所在的域,就知道这个跨域是被允许的
Access-Control-Allow-Origin: http://localhost:8081 

非简单的请求:

OPTIONS /put HTTP/1.1
Host: localhost:8080
Connection: keep-alive
Accept: */*
Access-Control-Request-Method: PUT
Origin: http://localhost:8081
Referer: http://localhost:8081/index.html

请求方法是OPTIONS,请求头Origin就告诉服务端当前页面所在域,请求头Access-Control-Request-Methods告诉服务器端即将发起的跨域请求所使用的方法。服务端对此进行判断,如果允许即将发起的跨域请求,则会给出如下响应:

HTTP/1.1 200
Access-Control-Allow-Origin: http://localhost:8081
Access-Control-Request-Methods: PUT
Access-Control-Max-Age: 3600

Access-Control-Allow-Metbods字段表示允许的跨域方法,Access-Control-Max-Age 字段表示预检请求的有效期,单位为秒,在有效期内如果发起该跨域请求,则不用再次发起预检请求。预检请求结束后,接下来就会发起-一个真正的跨域请求,跨域请求和前面的简单请求跨域步骤类似。

12.2 Spring跨域解决方案

  1. @CrossOrigin

    可以添加到方法上,或者Controller上,来支持跨域

    @Controller
    public class HelloController {
        // origins允许的域,*代表所有。 只写一个@CrossOrigin,默认代表允许所有域所有请求方法
        @CrossOrigin(origins = "http://localhost:8081") 
        @PostMapping("/post")
        @ResponseBody
        public String post() {
            return "hello post";
        }
    }
    
    • allowCredentials:浏览器是否应发生凭证信息,如Cookie
    • allowedHeaders:请求被允许的请求头字段,*代表所有
    • exposeHeaders:哪些响应头可以作为响应的一部分暴露出来
    • origins:允许可访问的域列表,*代表所有
    • maxAge:预检请求的有效期,默认1800(以秒为单位)
    • methods:允许的请求方法,*代表所有
  2. addCrosMapping

    全局配置方法

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
        @Override
        public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**") // 处理的请求地址
                        .allowedMethods("*")
                        .allowedOrigins("*")
                        .allowedHeaders("*")
                        .allowCredentials(false) // 这两个可以不写
                        .exposedHeaders("")
                        .maxAge(1800);
        }
    }
    
  3. CorsFilter

    Spring提供的处理跨域的Filter

    @Configuration
    public class WebMvcConfig {
        @Bean
        public FilterRegistrationBean<CorsFilter> corsFilter() {
            FilterRegistrationBean<CorsFilter> registrationBean = new FilterRegistrationBean<>();
            CorsConfiguration corsConfiguration = new CorsConfiguration();
            corsConfiguration.setAllowedMethods(Arrays.asList("*"));
            corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
            corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
            corsConfiguration.setMaxAge(1800L);
            UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
            source.registerCorsConfiguration("/**", corsConfiguration);
            registrationBean.setFilter(new CorsFilter(source));
            registrationBean.setOrder(-1); // -1表示在所有Filter之前执行,如果多个都是-1则按加载顺序执行
            return registrationBean;
        }
    }
    

    这三种的原理是一样的


12.3 Spring Security跨域解决方案

当项目引入Spring Security后,上面的@CrossOrigin和addCorsMappings都失效了,CorsFilter有没有失效要看设置的优先级,如果优先于Spring Security的过滤链,则依然有效,反之无效。(经过测试,即使上面设置-1也会失效)

解决方案:⭐⭐⭐

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 跨域问题
        http.authorizeRequests()
            .anyRequest().authenticated()
            //.......
            .and()
            .cors().configurationSource(corsConfigurationSource())
            .and()
            .csrf().disable();
    }

    // 和上面的CorsFilter的配置一样,只是不需要我们创建了!
    public UrlBasedCorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
        corsConfiguration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }
}

13. 异常处理

Spring Security只是解决自己的认证和授权异常,其他的全局异常还是交给SpringMvc去处理的

Spring Security中异常只要分为两类

  • AuthenticationException 认证异常
  • AccessDeniedException 授权异常
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 异常处理
        http.authorizeRequests()
            .anyRequest().authenticated()
            //.......
            .and()
            .exceptionHandling() // 前后端分离异常处理
            .authenticationEntryPoint(((request, response, e) -> {  // 认证异常,返回一些json也行
                response.setStatus(HttpStatus.UNAUTHORIZED.value());
                response.getWriter().write("尚未认证,请认证操作");
            }))
            .accessDeniedHandler((request, response, e) -> {  // 授权异常
                response.setStatus(HttpStatus.FORBIDDEN.value()); 
                response.getWriter().write("无权访问");
            })
            .and()
            .csrf().disable();
    }
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值