springboot security book

 

spring security几个概念


这个概念是通用的, 而不仅仅在Spring Security中 , 在Shiro中也是一样的.

 

1、框架介绍

Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括

"认证"(Authentication验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。
"授权"(Authorization)   用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。

 

记忆小窍门:e 在o 之前,所以先有Authentication再有Authorization ,认证在前,授权在后

 

Spring Security其实就是用filter,多请求的路径进行过滤。

(1)如果是基于Session,那么Spring-security会对cookie里的sessionid进行解析,找到服务器存储的sesion信息,然后判断当前用户是否符合请求的要求。

(2)如果是token,则是解析出token,然后将当前请求加入到Spring-security管理的权限信息中去

 

Spring Security的几个Web安全关键点

1. 登陆/注销
  HttpSecurity配置登陆、注销功能
2. Thymeleaf提供的SpringSecurity标签支持
  需要引入thymeleaf-extras-springsecurity4
  sec:authentication ="name" 获得当前用户的用户名
  sec:authorize = "hasRole('ADMIN')" 当前用户必须拥有ADMIN权限时才会显示标签内容
3. remember me
  表单添加remember-me的checkbox
  配置启用remember-me功能
4. CSRF(Cross-site request forgery)跨站请求伪造
  HttpSecurity启用csrf功能,会为表单添加_csrf的值,提交携带来预防CSRF; 

 

我们仅需引入spring-boot-starter-security模块,进行少量的配置,即可实现强大的安全管理。


security中几个重要的类如下:

WebSecurityConfigurerAdapter:自定义Security策略
AuthenticationManagerBuilder:自定义认证策略
@EnableWebSecurity:开启WebSecurity模式 (在@Controller注解的类上追加)

 

 

SpringSecurity在SpringBoot中使用

springsecurity在什么都不配的情况下,默认帐号是user, 密码在启动日志中随机生成uuid,如下形式

2019-06-04 09:44:52.852  INFO 33512 --- [           main] b.a.s.AuthenticationManagerConfiguration : 
Using
default security password: bc4c813c-b8f9-4cdb-9ca9-b406d3492da9

 

 

配置MySecurityConfig.java

官方说明自定义SecurityConfig替换默认配置参考链接: https://docs.spring.io/spring-security/site/docs/5.2.0.BUILD-SNAPSHOT/reference/htmlsingle/#oauth2resourceserver-sansboot

protected void configure(HttpSecurity http) {
    http
        .authorizeRequests()
            .anyRequest()
       .authenticated()
            .and()
           .oauth2ResourceServer()
            .jwt();
}

 

自定义MySecurityConfig.java

@EnableWebSecurity //该注解本身就包含@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);
        //定制授权规则
        http.authorizeRequests().antMatchers("/").permitAll().
                antMatchers("/level1/**").hasRole("VIP1").
                antMatchers("/level2/**").hasRole("VIP2").
                antMatchers("/level3/**").hasRole("VIP3");
    }

}

 

anonymous( )匿名的意思是未登录可以访问, 登录了反而不能访问

 

配置自动生成的登录页

开启自动配置的 /login 登录页面,如果不配置, 那么无授权时会报403 Access is denied错误,且页面也不知道跳哪,因为还没有开启自动配置的login登录页面
,默认使用的是/login 和 /login?error, 现在改成/userlogin,会经过 @GetMapping("/userlogin")注解的方法.

也可以配置登录页及登录时的帐号密码字段是什么等.

http.formLogin().loginPage("/login").usernameParameter("username").passwordParameter("password");

 

配置登出界面

开启自动配置的注销功能,默认访问/logout表示注销,会清空session及cookie,注销成功后返回/login?logout页面

http.logout().logoutSuccessUrl("/");//自定义注销成功后跳转到/页面

 

配置认证规则

/**
 * 定义认证规则,管理帐号/密码
 *
 * @param auth
 * @throws Exception
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //auth.jdbcAuthentication(); //一般用jdbc

    //学习用内存认证
    auth.inMemoryAuthentication()
            .withUser("bobo").password("123456").roles("VIP1")
            .and().withUser("sisi").password("123456").roles("VIP1", "VIP2")
            .and().withUser("xixi").password("123456").roles("VIP1", "VIP2", "VIP3")
            .and().passwordEncoder(new CustomPasswordEncoder());
}

在Spring Security 5.0(不包括)之前,PasswordEncoder 的默认值为 NoOpPasswordEncoder 既表示为纯文本密码,在实际的开发过程中 PasswordEncoder 大多数都会设置为 BCryptPasswordEncoder 

但在spring security5开始默认使用了加密规则 , 如果仍要使用明文密码, 则需要新建一个 PasswordEncoder 并实现 PasswordEncoder 接口,重新 里面的两个方法,并定义为明文的加密方式,具体内容如下:

public class CustomPasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence charSequence) {
        return charSequence.toString();
    }

    @Override
    public boolean matches(CharSequence charSequence, String s) {
        return s.equals(charSequence.toString());
    }
}

然后再加上该自定义密码编码器

/**
 * 定义认证规则,管理帐号/密码
 *
 * @param auth
 * @throws Exception
 */
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //auth.jdbcAuthentication(); //一般用jdbc

    //学习用内存认证
    auth.inMemoryAuthentication()
            .withUser("bobo").password("123456").roles("VIP1")
            .and().withUser("sisi").password("123456").roles("VIP1", "VIP2")
            .and().withUser("xixi").password("123456").roles("VIP1", "VIP2", "VIP3")
            .and().passwordEncoder(new CustomPasswordEncoder());
}

 

使用springSecurity标签

在pom.xml中引入thymeleaf依赖的springSecurity标签的插件

        <!--在thymeleaf中使用认证标签需要的额外依赖-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity4</artifactId>
            <version>3.0.2.RELEASE</version>
        </dependency>

具体的最新版本号可以根据maven仓库来查询,特别注意的是thymeleaf-extras-springsecurity有3、4、5的版本区别,具体需要看项目使用到的Spring Security版本

thymeleaf-extras-springsecurity3

for

integration with Spring Security 3.x

thymeleaf-extras-springsecurity4

for

integration with Spring Security 4.x

thymeleaf-extras-springsecurity5

for

integration with Spring Security 5.x

 

 

html标签体中引入security规范

<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">

 

 

thymeleaf中显示用户信息

<div sec:authorize="isAuthenticated()">
    <h2> 您好 , <span sec:authentication="name"></span> 您的角色是 <span sec:authentication="principal.authorities"></span>
    </h2>
</div>

 

 

thymeleaf中显示判断是否有权限

<div sec:authorize="hasRole('VIP1')">
        <h3>普通武功秘籍</h3>
        <ul>
            <li><a th:href="@{/level1/1}">罗汉拳</a></li>
            <li><a th:href="@{/level1/2}">武当长拳</a></li>
            <li><a th:href="@{/level1/3}">全真剑法</a></li>
        </ul>
    </div>

 

 

开启记住我功能

 开启记住我功能登录成功,会从服务器返回添加名为remember-me的cookie指令, 以后访问页面都会带上该cookie, 只要服务器通过检查就可以免登录了,默认14天后失效

http.rememberMe().rememberMeParameter("remember-me");

 

 

 

 

此时使用/logout会一并清除名为remember-me的cookie , 因为/logout请求在header头中携带了Max-Age=0参数

 

 

 

 

 

自定义登录页,使用/userlogin

 

        //默认使用的是/login 和 /login?error, 现在改成/userlogin,会经过KungfuController的 @GetMapping("/userlogin")注解的方法
        http.formLogin().loginPage("/userlogin");//.usernameParameter("username").passwordParameter("password");

 

默认get形式的/login来到登录页, post形式的/login用来表单登录.

当自定义了登录页面/userlogin后,那么get形式的/userlogin来到登录页, post形式的/userlogin用来表单登录. 见源码注释说明如下:

 

 

最终项目结构

 

核心配置类MySecurityConfig.java内容

package com.example.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;

@EnableWebSecurity //该注解本身就包含@Configuration
public class MySecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //super.configure(http);

        //定制授权规则
        http.authorizeRequests().antMatchers("/").permitAll().
                antMatchers("/level1/**").hasRole("VIP1").
                antMatchers("/level2/**").hasRole("VIP2").
                antMatchers("/level3/**").hasRole("VIP3");

        //参见 HttpSecurity 类的    public FormLoginConfigurer<HttpSecurity> formLogin() 方法注解
        //开启自动配置的 /login 登录页面,如果不配置, 那么无授权时会报403 Access is denied错误,且页面也不知道跳哪,因为还没有开启自动配置的login登录页面
        //默认使用的是/login 和 /login?error, 现在改成/userlogin,会经过KungfuController的 @GetMapping("/userlogin")注解的方法
        http.formLogin().loginPage("/userlogin");//.usernameParameter("username").passwordParameter("password");

        //开启自动配置的注销功能,默认访问/logout表示注销,会清空session及cookie,注销成功后返回/login?logout页面
        http.logout().logoutSuccessUrl("/");//自定义注销成功后跳转到/页面

        //开启记住我功能登录成功,会从服务器返回添加名为remember-me的cookie指令, 以后访问页面都会带上该cookie, 只要服务器通过检查就可以免登录了,默认14天后失效
        http.rememberMe().rememberMeParameter("remember-me");
    }

    /**
     * 定义认证规则,管理帐号/密码
     *
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //auth.jdbcAuthentication(); //一般用jdbc

        //学习用内存认证
        auth.inMemoryAuthentication()
                .withUser("bobo").password("123456").roles("VIP1")
                .and().withUser("sisi").password("123456").roles("VIP1", "VIP2")
                .and().withUser("xixi").password("123456").roles("VIP1", "VIP2", "VIP3");
    }


}

 

直接关闭所有Security的所有拦截功能

http.headers().frameOptions().disable().and().authorizeRequests().antMatchers("/**", "/login*").permitAll(); // 所有用户都可以访问
http.csrf().disable().authorizeRequests().anyRequest().permitAll().and().logout().permitAll(); //禁用security的 csrf功能

 

SpringSecurity默认是禁止接收POST请求的,而GET是默认可以的,官网给出两个解决方案:1是发送请求时带上CSRF的token,2是不推荐的做法(把SpringSecurity的CSRF功能关掉),不然很有可能是get请求正常, 但post请求报403 forbidden

其它参考: Spring Boot与Spring Security整合后post数据不了,403拒绝访问==>https://blog.csdn.net/sinat_28454173/article/details/52251004

springboot security版本差异

 

帐号密码配置差异(亲测)

在security 4版本中(springboot1.5中使用), application.yml中简易帐号密码配置如下,没有spring:

security:
  user:
    name: bobo
    password: 123456

而在security 5版本中(springboot2开始使用), application.yml中帐号密码配置如下, 多了一层spring: 

spring:
  security:
    user:
      name: bobo
      password: 123456

 

禁用security

pom.xml中如果存在以下依赖

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

启动时控制台会随机生成登录密码, 打印如下:

Using generated security password: 17718027-34f3-444c-9623-7243038d0af9

请求接口时会跳到登录页 Please sign in

 

 禁用spring security两种方法, 配置类或启动类上加上如下(二选一)

@EnableAutoConfiguration(exclude = {SecurityAutoConfiguration.class}) 或者   @SpringBootApplication(exclude = {SecurityAutoConfiguration.class })

 

 

SpringSecurity导致同一页面不同http请求session.getId()不一致的问题

@date20210706 from zq

/**
* ALWAYS:总是创建HttpSession
* IF_REQUIRED:Spring Security只会在需要时创建一个HttpSession
* NEVER:Spring Security不会创建HttpSession,但如果它已经存在,将可以使用HttpSession
* STATELESS:Spring Security永远不会创建HttpSession,它不会使用HttpSession来获取SecurityContext
*/
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);   // 之前将SpringSecurity的session策图换成了STATELESS, 永不使用httpsession,导致了session不一致, 需要改回SessionCreationPolicy.IF_REQUIRED

package com.rosellete.iescp.web.starter.configuration;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

import com.xxx.web.starter.filter.JwtAuthenticationTokenFilter;
import com.xxx.web.starter.handler.SimpleAuthenticatingEntryPointHandler;
import com.xxx.web.starter.handler.SimpleAuthenticatingFailureHandler;

/**
 * Spring Security 配置
 * 通过SpringSecurity的配置,将JWTLoginFilter,JWTAuthenticationFilter组合在一起
 *
 * 
 *
 */
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    private static final Logger logger = LoggerFactory.getLogger(WebSecurityConfig.class);

    @Autowired
    private SimpleAuthenticatingEntryPointHandler simpleAuthenticatingEntryPointHandler;

    @Autowired
    private SimpleAuthenticatingFailureHandler simpleAuthenticatingFailureHandler;

    private static String[] AUTH_WHITELIST = {};
    private static String[] STATIC_AUTH_WHITELIST = {};
    static {
        Properties props = new Properties();
        try {
            props.load(WebSecurityConfigurerAdapter.class.getResourceAsStream("/matcherspermit.properties"));
            String authWhiteList = props.getProperty("AUTH_WHITELIST");
            String staticAuthWhiteList = props.getProperty("STATIC_AUTH_WHITELIST");
            logger.info("读取Properties中AUTH_WHITELIST值为:{}" , authWhiteList);
            logger.info("读取Properties中STATIC_AUTH_WHITELIST值为:{}" , staticAuthWhiteList);
            if(StringUtils.isNotBlank(authWhiteList)) {
                String [] ss = authWhiteList.split(";");
                List<String> arr = new ArrayList<>();
                for(String s : ss) {
                    if(s.trim().length() > 0) {
                        arr.add(s.trim());
                    }
                }
                AUTH_WHITELIST = arr.toArray(new String[arr.size()]);
            }
            if(StringUtils.isNotBlank(staticAuthWhiteList)) {
                String [] ss = staticAuthWhiteList.split(";");
                List<String> arr = new ArrayList<>();
                for(String s : ss) {
                    if(s.trim().length() > 0) {
                        arr.add(s.trim());
                    }
                }
                STATIC_AUTH_WHITELIST = arr.toArray(new String[arr.size()]);
            }
        } catch (IOException e) {
            logger.error("读取配置文件MatchersPermit.properties错误" , e);
        }
    }
    /**
     * HTTP 验证规则
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http
                .antMatcher("/**").authorizeRequests();

        // 禁用CSRF 开启跨域
        http.csrf().disable().cors();

        // 未登录认证异常
        http.exceptionHandling().authenticationEntryPoint(simpleAuthenticatingEntryPointHandler);
        // 登录过后访问无权限的接口时自定义403响应内容
        http.exceptionHandling().accessDeniedHandler(simpleAuthenticatingFailureHandler);

        // 不创建会话 - 即通过前端传token到后台过滤器中验证是否存在访问权限
        /**
         * ALWAYS:总是创建HttpSession
         * IF_REQUIRED:Spring Security只会在需要时创建一个HttpSession
         * NEVER:Spring Security不会创建HttpSession,但如果它已经存在,将可以使用HttpSession
         * STATELESS:Spring Security永远不会创建HttpSession,它不会使用HttpSession来获取SecurityContext
         */
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
        // 允许匿名的url - 可理解为放行接口 - 除配置文件忽略url以外,其它所有请求都需经过认证和授权
        registry.antMatchers(AUTH_WHITELIST).permitAll();
        // 其余所有请求都需要认证
        registry.anyRequest().authenticated();
        // 自动登录 - cookie储存方式
        registry.and().rememberMe();
        // 防止iframe 造成跨域
        registry.and().headers().frameOptions().disable();
        // 自定义过滤器在登录时认证用户名、密码
        http.addFilterBefore(this.getJwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class);
        // 禁用缓存
        http.headers().cacheControl();
    }

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

    @Bean
    public JwtAuthenticationTokenFilter getJwtAuthenticationTokenFilter() throws Exception {
        return new JwtAuthenticationTokenFilter();
    }

    /**
     * 忽略拦截url或静态资源文件夹
     */
    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().antMatchers(HttpMethod.GET, STATIC_AUTH_WHITELIST);
    }
}

 

 

spring redis session导致的session.hashCode()不一致问题

在使用了了 EnableRedisHttpSession  之后 session的getId()仍是一致的, 但是session.hashCode()已不一致, 是因为每次从redis中反序列化后重新生成了session对象,所以不一致,但是对getId()没有任何影响.

spring redis session==>https://www.cnblogs.com/whatlonelytear/p/14976390.html

 

SpringSecurity - 整合JWT使用 Token 认证授权

https://www.yuque.com/aaliyubo/mohgoq/qy1809

 

登录问题

Spring Security在登录验证中增加额外数据(如验证码)==>https://www.cnblogs.com/phoenix-smile/p/5666686.html

spring security前台登录如何传递多个参数要后台验证==>https://bbs.csdn.net/topics/390493958    (在username里用_分割啊哈哈哈)

$$$$$$$$$$spring security controller层实现登陆==>https://blog.csdn.net/qq_34675369/article/details/91499798

我的项目git地址

https://gitee.com/KingBoBo/springboot-05-security

 最好的教程

在SpringBoot 中使用Security安全框架==>https://blog.csdn.net/qwe86314/article/details/89509765

 

SpringSecurity系列学习(三):认证流程和源码解析==>zhuanlan.zhihu.com/p/458853290

 

 

相关系列文章

SpringBoot 整合 oauth2(三)实现 token 认证==>https://www.jianshu.com/p/19059060036b

SpringBoot 整合 Security(一)实现用户认证并判断返回json还是view==>https://www.jianshu.com/p/18875c2995f1

SpringBoot 整合 Security(二)实现验证码登录==>https://www.jianshu.com/p/9d08c767b33e

 

相关系列文章2

posted @ 2019-05-20 14:38  苦涩泪滴  阅读(1571)  评论(3编辑  收藏  举报