7-Spring Boot的安全管理

一、Spring Security基础

1.Spring Security介绍

a.Spring Security是基于Spring生态圈的,用于提供安全访问控制解决方案的框架。

b.Spring Security的安全管理有两个重要概念,分别是Authentication(认证)和Authorization(授权)。

2.Spring Boot整合Spring Security实现的安全管理功能

a.MVC Security是Spring Boot整合Spring MVC框架搭建的Web应用的安全管理。

b.WebFlux Security是Spring Boot整合Spring WebFlux框架搭建的Web应用的安全管理。

c.OAuth2是大型项目的安全管理框架,可以实现第三方认证、单点登录等功能。

d.Actuator Security用于对项目的一些运行环境提供安全监控,例如Health健康信息、Info运行信息等,它主要作为系统指标供运维人员查看管理系统的运行情况。

二、Spring Security入门

1.基础环境搭建

a.创建Spring Boot项目

引入Web和Thymeleaf的依赖启动器

b.引入页面Html资源文件

在项目的resources下templates目录中,引入案例所需的资源文件(下载地址),项目结构如下

c.编写Web控制层

@Controller
public class FilmController {

    //  影片详情页
    @GetMapping("/detail/{type}/{path}")
    public String toDetail(@PathVariable("type")String type, @PathVariable("path")String path) {

        return "detail/"+type+"/"+path;

    }

}

至此,使用Spring Boot整合Spring MVC框架实现了一个传统且简单的Web项目,

目前项目没有引入任何的安全管理依赖,也没有进行任何安全配置,

项目启动成功后,通过http://localhost:8080访问首页,单击影片进入详情详情页。

2.开启安全管理效果测试

a.添加spring-boot-starter-security启动器

一旦项目引入spring-boot-starter-security启动器,MVC Security和WebFlux Security负责的安全功能都会立即生效

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

b.项目启动测试

项目启动时会在控制台Console中自动生成一个安全密码

如果是热部署重启项目,可能不会有安全密码,那就关闭项目,再启动

浏览器访问http://localhost:8080/查看项目首页,会跳转到一个默认登录页面。

因为添加了Security依赖后,会进行spring security的自动化配置,需要先登录,才能访问首页,Spring Security会自带一个默认的登录页面。

随意输入一个错误的用户名和密码,会出现错误提示

Security会默认提供一个可登录的用户信息,其中用户名为user,密码随机生成,

这个密码会随着项目的每次启动随机生成并打印在控制台上,在登录页面输入用户名和密码。

这种默认安全管理方式存在诸多问题,例如:

只有唯一的默认登录用户user、密码随机生成且过于暴露、登录页面及错误提示页面不是我们想要的等。

三、MVC Security安全配置

1.MVC Security安全配置简介

项目引入spring-boot-starter-security依赖启动器,MVC Security安全管理功能就会自动生效,其默认的安全配置是在SecurityAutoConfiguration和UserDetailsServiceAutoConfiguration中实现的。

SecurityAutoConfiguration导入并自动化配置SpringBootWebSecurityConfiguration用于启动Web安全管理.

UserDetailsServiceAutoConfiguration用于配置用户身份信息.

这两个类的位置:

先看spring-boot-autoconfigure-2.0.7.RELEASE.jar下的/META-INF/spring.factories文件,发现org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration,\

org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration,\

发现在org.springframework.boot.autoconfigure.security.servlet这个包下,

2.关闭Sercurity提供的Web应用默认安全配置

1.要完全关闭Security提供的Web应用默认安全配置,可以自定义WebSecurityConfigurerAdapter类型的Bean组件以及自定义UserDetailsService、AuthenticationProvider或AuthenticationManager类型的Bean组件。

2.另外,可以通过自定义WebSecurityConfigurerAdapter类型的Bean组件来覆盖默认访问规则。

3.WebSecurityConfigurerAdapter类的主要方法及说明

方法

描述

configure(AuthenticationManagerBuilder auth)

定制用户认证管理器来实现用户认证

configure(HttpSecurity http)

定制基于HTTP请求的用户访问控制

四、自定义用户认证

1.内存身份认证

a.自定义WebSecurityConfigurerAdapter配置类

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

}

注:

@EnableWebSecurity注解是一个组合注解,主要包括@Configuration注解、@Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class})注解和@EnableGlobalAuthentication注解

b.使用内存进行身份认证

SecurityConfig类中重写configure(AuthenticationManagerBuilder auth)方法,并在该方法中使用内存身份认证的方式自定义了认证用户信息。定义用户认证信息时,设置了两个用户名和密码以及对应的角色信息。

@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter{

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        auth.inMemoryAuthentication().passwordEncoder(encoder)
                .withUser("shitou").password(encoder.encode("123456")).roles("common")
                .and()
                .withUser("李四").password(encoder.encode("123456")).roles("vip");
    }

}

c.效果测试

重启项目进行效果测试,项目启动成功后,仔细查看控制台打印信息,发现没有默认安全管理时随机生成的密码了。通过浏览器访问http://localhost:8080/

2.JDBC身份认证

a.数据准备

# 选择使用数据库
USE springbootdata;

# 创建表t_customer并插入相关数据
DROP TABLE IF EXISTS `t_customer`;
CREATE TABLE `t_customer` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(200) DEFAULT NULL,
  `password` varchar(200) DEFAULT NULL,
  `valid` tinyint(1) NOT NULL DEFAULT '1',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

INSERT INTO `t_customer` VALUES ('1', 'shitou', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '1');
INSERT INTO `t_customer` VALUES ('2', '李四', '$2a$10$5ooQI8dir8jv0/gCa1Six.GpzAdIPf6pMqdminZ/3ijYzivCyPlfK', '1');

# 创建表t_authority并插入相关数据
DROP TABLE IF EXISTS `t_authority`;
CREATE TABLE `t_authority` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `authority` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

INSERT INTO `t_authority` VALUES ('1', 'ROLE_common');
INSERT INTO `t_authority` VALUES ('2', 'ROLE_vip');

# 创建表t_customer_authority并插入相关数据
DROP TABLE IF EXISTS `t_customer_authority`;
CREATE TABLE `t_customer_authority` (
  `id` int(20) NOT NULL AUTO_INCREMENT,
  `customer_id` int(20) DEFAULT NULL,
  `authority_id` int(20) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;

INSERT INTO `t_customer_authority` VALUES ('1', '1', '1');
INSERT INTO `t_customer_authority` VALUES ('2', '2', '2');

b.添加JDBC连接数据库的依赖启动器

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <scope>runtime</scope>
</dependency>

c.进行数据库连接配置

spring.datasource.url=jdbc:mysql://localhost:3306/springbootdata?serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=root

d.使用JDBC进行身份认证

在SecurityConfig 类中的configure(AuthenticationManagerBuilder auth)方法中使用JDBC身份认证的方式进行自定义用户认证,使用JDBC身份认证时,首先需要对密码进行编码设置(必须与数据库中用户密码加密方式一致);然后需要加载JDBC进行认证连接的数据源DataSource;最后,执行SQL语句,实现通过用户名username查询用户信息和用户权限。

    @Autowired
    private DataSource dataSource;
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String userSQL ="select username,password,valid from t_customer "+ "where username = ?";
        String authoritySQL="select c.username,a.authority from t_customer c, "+"t_authority a,t_customer_authority ca where "+"ca.customer_id=c.id and ca.authority_id=a.id and c.username =?";
       auth.jdbcAuthentication().passwordEncoder(encoder).dataSource(dataSource)
      .usersByUsernameQuery(userSQL).authoritiesByUsernameQuery(authoritySQL);  

}

e.效果测试

先停止运行再启动或者直接relaunch下。在浏览器中http://localhost:8080/

如果热部署,可能出现下面错误

比如a bean of type 'javax.sql.DataSource' that could not be found.

3.UserDetailsService 身份认证

a.要用到jpa和redis,所以加入两个依赖,如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

b.处理类要用到其他类,项目结构如下

c.上述类的主要代码如下:

@Entity(name = "t_authority ")
public class Authority implements Serializable{

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String authority ;
    
    
}
@Entity(name = "t_customer")
public class Customer implements Serializable{

    private static final long serialVersionUID = 1L;

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

    private String username;
    private String password;


}
public interface AuthorityRepository extends JpaRepository<Authority,Integer> {

    @Query(value = "select a.* from t_customer c,t_authority a,t_customer_authority ca where ca.customer_id=c.id and ca.authority_id=a.id and c.username =?1",nativeQuery = true)
    public List<Authority> findAuthoritiesByUsername(String username);

}
public interface CustomerRepository extends JpaRepository<Customer,Integer> {

    Customer findByUsername(String username);

}
//CustomerService业务处理类,用来通过用户名获取用户及权限信息
@Service
public class CustomerService {

    @Autowired
    private CustomerRepository customerRepository;

    @Autowired
    private AuthorityRepository authorityRepository;

    @Autowired
    private RedisTemplate redisTemplate;


    // 业务控制:使用唯一用户名查询用户信息
    public Customer getCustomer(String username){

        Customer customer=null;
        Object o = redisTemplate.opsForValue().get("customer_"+username);
        if(o!=null){
            customer=(Customer)o;
        }else {
            customer = customerRepository.findByUsername(username);
            if(customer!=null){
                redisTemplate.opsForValue().set("customer_"+username,customer);
            }

        }
        return customer;
    }

    // 业务控制:使用唯一用户名查询用户权限
    public List<Authority> getCustomerAuthority(String username){

        List<Authority> authorities=null;
        Object o = redisTemplate.opsForValue().get("authorities_"+username);
        if(o!=null){
            authorities=(List<Authority>)o;
        }else {
            authorities=authorityRepository.findAuthoritiesByUsername(username);
            if(authorities.size()>0){
                redisTemplate.opsForValue().set("authorities_"+username,authorities);
            }
        }
        return authorities;
    }
}

d.定义UserDetailsService用于封装认证用户信息

UserDetailsService是Security提供的进行认证用户信息封装的接口,该接口提供的loadUserByUsername(String s)方法用于通过用户名加载用户信息。使用UserDetailsService进行身份认证的时,自定义一个UserDetailsService接口的实现类,通过loadUserByUsername(String s)方法调用用户业务处理类中已有的方法进行用户详情封装,返回一个UserDetails封装类,来供Security认证使用。

自定义一个接口实现类UserDetailsServiceImpl进行用户认证信息UserDetails封装,重写了UserDetailsService接口的loadUserByUsername(String s)方法,在该方法中,使用CustomerService业务处理类获取用户的用户信息和权限信息,并通过UserDetails进行认证用户信息封装。

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

    @Autowired
    private CustomerService customerService;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        // 通过业务方法获取用户及权限信息
        Customer customer = customerService.getCustomer(s);
        List<Authority> authorities = customerService.getCustomerAuthority(s);

        // 对用户权限进行封装
        List<SimpleGrantedAuthority> list = authorities.stream().map(authority -> new SimpleGrantedAuthority(authority.getAuthority())).collect(Collectors.toList());

        // 返回封装的UserDetails用户详情类
        if(customer!=null){
            UserDetails userDetails= new User(customer.getUsername(),customer.getPassword(),list);
            return userDetails;
        } else {
            // 如果查询的用户不存在(用户名不存在),必须抛出此异常
            throw new UsernameNotFoundException("当前用户不存在!");
        }

    }

}

e.在SecurityConfig中使用UserDetailsService进行身份认证

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        auth.userDetailsService(userDetailsService).passwordEncoder(encoder);

    }

如果是springboot3.0:

@EnableWebSecurity
@Configuration
public class SecurityConfig {    
    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Bean
    public AuthenticationManager authenticationManager() throws Exception {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setPasswordEncoder(bCryptPasswordEncoder());
        authenticationProvider.setUserDetailsService(userDetailsService);
        ProviderManager providerManager = new ProviderManager(Arrays.asList(authenticationProvider));
        return providerManager;
    }
    @Bean
    BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

 f.效果测试

重启项目进行效果测试,项目启动成功后,通过浏览器访问http://localhost:8080/

五、自定义用户授权管理(下载:此项目已完成jdbc认证)

1.自定义用户访问控制

a.在自定义配置类SecurityConfig中重写configure(HttpSecurity http)方法

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
        .antMatchers("/").permitAll()
        .antMatchers("/detail/common/**").hasRole("common")
        .antMatchers("/detail/vip/**").hasRole("vip")
        .anyRequest().authenticated()
        .and()
        .formLogin();
}

路径是“/”,直接放行。

路径是"/detail/common/**",只有用户角色是common才允许访问。

路径是"/detail/vip/**",只有用户角色是vip才允许访问。

其他请求要先登录认证后才放行。

b.效果测试

重启项目进行效果测试,项目启动成功后,通过浏览器访问http://localhost:8080/

项目首页单击普通电影或者VIP专享电影名称查询电影详情

在此登录界面输入普通用户的用户名和密码,访问普通电影

在项目首页中单击VIP专享电影名称查看影片详情,

在查看VIP电影详情时,页面会出现403 Forbidden的错误信息

2.自定义用户登录

a.在项目的resources/ templates目录下创建login/login.html,核心代码如下

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <title>用户登录界面</title>
    <link th:href="@{/login/css/bootstrap.min.css}" rel="stylesheet">
    <link th:href="@{/login/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
    <form class="form-signin" th:action="@{/userLogin}" th:method="post" >
        <img class="mb-4" th:src="@{/login/img/login.jpg}" width="72px" height="72px">
        <h1 class="h3 mb-3 font-weight-normal">请登录</h1>
        <!-- 用户登录错误信息提示框 -->
        <div th:if="${param.error}" style="color: red;height: 40px;text-align: left;font-size: 1.1em">
            <img th:src="@{/login/img/loginError.jpg}" width="20px">用户名或密码错误,请重新登录!
        </div>
        <input type="text" name="name" class="form-control" placeholder="用户名" required="" autofocus="">
        <input type="password" name="pwd" class="form-control" placeholder="密码" required="">
        <div class="checkbox mb-3">
            <label>
                <input type="checkbox" name="rememberme"> 记住我
            </label>
        </div>
        <button class="btn btn-lg btn-primary btn-block" type="submit" >登录</button>
        <p class="mt-5 mb-3 text-muted">Copyright© 2019-2020</p>
    </form>
</body>
</html>

通过<form>标签定义了一个用户登录功能,且登录数据以POST方式向“/userLogin”路径进行提交。

其中,登录表单中的用户名参数和密码参数可以自行定义;登录数据提交方式必须为post,提交的路径也可以自行定义。

b.拷贝静态资源文件到项目resources下的static目录中

c.自定义用户登录跳转

在之前创建的FilmController类中添加一个跳转到登录页面login.html的方法

@GetMapping("/userLogin")
public String toLoginPage() {
    return "login/login";
}

注:Spring Security默认使用Get方式的“/login”请求用于向登录页面跳转,默认使用Post方式的“/login”请求用于对登录后的数据进行处理。因此,自定义用户登录控制时,需要提供向用户登录页面跳转的方法,且自定义的登录页跳转路径必须与数据处理提交路径一致

d.自定义用户登录控制

打开SecurityConfig类,重写configure(HttpSecurity http)方法实现用户登录控制

http.authorizeRequests().antMatchers("/").permitAll()
    // 需要对static文件夹下静态资源进行统一放行
    .antMatchers("/login/**").permitAll()
    .antMatchers("/detail/common/**").hasRole("common")
    .antMatchers("/detail/vip/**").hasRole("vip")
    .anyRequest().authenticated();

http.formLogin()
    .loginPage("/userLogin").permitAll()
    .usernameParameter("name").passwordParameter("pwd")
    .defaultSuccessUrl("/")
    .failureUrl("/userLogin?error");

上面.usernameParameter("name").passwordParameter("pwd")中的name和pwd与login.html中的文本框name属性值一致, 如下:

e.效果测试

重启项目进行效果测试,项目启动成功后,通过浏览器访问http://localhost:8080/

会直接进入到项目首页,在项目首页,单击普通电影或者VIP专享电影名称查询电影详情.

输入错误的账号信息

3.自定义用户退出

a.添加自定义用户退出链接

在index.html添加自定义用户退出链接

<form th:action="@{/mylogout}" method="post">
    <input th:type="submit" th:value="注销" />
</form>

注:Spring Boot项目中引入Spring Security框架后会自动开启CSRF防护功能(跨站请求伪造防护,此处作为了解即可,后续小节将详细说明),用户退出时必须使用POST请求;如果关闭了CSRF防护功能,那么可以使用任意方式的HTTP请求进行用户注销。

b.自定义用户退出控制

在SecurityConfig类,在configure(HttpSecurity http)方法中添加如下代码进行用户退出控制

http.logout()
    .logoutUrl("/mylogout")
    .logoutSuccessUrl("/");

c.效果测试

重启项目进行效果测试,项目启动成功后,通过浏览器访问http://localhost:8080/

先访问影片详情自动跳转到自定义的用户登录页面login.html,在登录界面输入正确的用户名和密码后,如下:

单击影片详情中的“返回”链接回到项目首页(此时用户仍处于登录状态),单击首页中的“注销”链接进行用户注销

4.登录用户信息获取

a.使用HttpSession获取用户信息及测试

在FilmeController类中新增一个用于获取当前会话用户信息的getUser()方法,在该方法中通过获取当前HttpSession的相关方法遍历并获取了会话中的用户信息。

在获取认证用户信息时,使用了Authentication的getPrincipal()方法,默认返回的也是一个Object对象,其本质是封装用户信息的UserDetails封装类,其中包括有用户名、密码、权限、是否过期等。

 /**
     * 通过传统的HttpSession获取Security控制的登录用户信息
     * @param session
     */
@GetMapping("/getuserBySession")
@ResponseBody
public void getUser(HttpSession session) {
    Enumeration<String> names = session.getAttributeNames();
    while (names.hasMoreElements()) {
        String element = names.nextElement();
        if (session.getAttribute(element) instanceof SecurityContextImpl) {
            SecurityContextImpl attribute = (SecurityContextImpl) session.getAttribute(element);
            System.out.println("element: " + element);
            System.out.println("attribute: " + attribute);
            Authentication authentication = attribute.getAuthentication();
            UserDetails principal = (UserDetails) authentication.getPrincipal();
            System.out.println(principal);
            System.out.println("username: " + principal.getUsername());
        }
    }
}

以Debug模式重启项目,浏览器访问http://localhost:8080/随意查看一个影片详情进行用户登录。

登录成功后,在保证当前浏览器未关闭的情况下,使用同一浏览器执行http://localhost:8080/getuserBySession来获取用户详情。

b.使用SecurityContextHolder获取用户信息

在FilmController控制类中新增一个获取当前会话用户信息的getUser2()方法

    /**
     * 通过Security提供的SecurityContextHolder获取登录用户信息
     */
    @GetMapping("/getuserByContext")
    @ResponseBody
    public void getUser2() {
        // 获取应用上下文
        SecurityContext context = SecurityContextHolder.getContext();
        System.out.println("userDetails: "+context);
        // 获取用户相关信息
        Authentication authentication = context.getAuthentication();
        UserDetails principal = (UserDetails)authentication.getPrincipal();
        System.out.println(principal);
        System.out.println("username: "+principal.getUsername());
    }

c.使用@AuthenticationPrincipal获取用户信息

@GetMapping("/getuserByAnnotation")
@ResponseBody
public void getUser(@AuthenticationPrincipal User user) {
	
	System.out.println(user);
    
}

d.使用Principal获取用户信息

@GetMapping("/getuserByPrincipal")
@ResponseBody
public void getUser(Principal user) {
	
	System.out.println(user);
    
}

 结果:

UsernamePasswordAuthenticationToken [Principal=
org.springframework.security.core.userdetails.User [Username=shitou, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_common]],
 Credentials=[PROTECTED], Authenticated=true,
 Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=EDE0B14447B32A83D32746831BCC6DC1], Granted Authorities=[ROLE_common]]

 e.使用Authentication获取用户信息

@GetMapping("/getuserByAuthentication")
@ResponseBody
public void getUser(Authentication user) {
	
	System.out.println(user);
    
}

结果:

UsernamePasswordAuthenticationToken [Principal=
org.springframework.security.core.userdetails.User [Username=shitou, Password=[PROTECTED], Enabled=true, AccountNonExpired=true, credentialsNonExpired=true, AccountNonLocked=true, Granted Authorities=[ROLE_common]], 
Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=0:0:0:0:0:0:0:1, SessionId=BF2BF5770E08BF8DCD63BC2AF3CED2B8], Granted Authorities=[ROLE_common]]

5.记住我功能(关闭再打开浏览器再访问时自动登录)

参考:SpringSecurity实现记住我_springsecurity记住我-CSDN博客

a.基于简单加密Token的方式

在之前创建的项目用户登录页login.html中新增一个记住我功能勾选框

<label>
    <input type="checkbox" name="rememberme"> 记住我
</label>

打开SecurityConfig类,重写configure(HttpSecurity http)方法进行记住我功能配置

http.rememberMe()
    .rememberMeParameter("rememberme")
    .tokenValiditySeconds(200);

基于简单加密Token的方式效果测试

重启项目进行效果测试,通过浏览器访问http://localhost:8080/userLogin

在登录界面输入正确的用户名和密码信息,同时勾选记住我功能,

就会默认跳转到项目首页index.html,关闭再打开浏览器访问项目首页,直接查看影片详情

b.基于持久化Token方式

需要在数据库中创建一个存储cookie信息的持续登录用户表

create table persistent_logins (
    username varchar(64) not null,
    series varchar(64) primary key,
    token varchar(64) not null,
    last_used timestamp not null
);

打开SecurityConfig类,重写configure(HttpSecurity http)方法的记住我功能 

        // 定制Remember-me记住我功能
        http.rememberMe()
            .rememberMeParameter("rememberme")
            .tokenValiditySeconds(200)
            // 对cookie信息进行持久化管理
            .tokenRepository(tokenRepository());

注入dataSource,增加一个方法

     @Autowired
    private DataSource dataSource;

    /**
     * 持久化Token存储
     * @return
     */
    @Bean
    public JdbcTokenRepositoryImpl tokenRepository(){
        JdbcTokenRepositoryImpl jr=new JdbcTokenRepositoryImpl();
        jr.setDataSource(dataSource);
        return jr;
    }

基于持久化Token方式效果测试

重启项目进行效果测试,通过浏览器访问项目首页,输入正确的账户信息,

勾选记住我后,跳转到项目首页index.html,

查看数据库中创建的存储cookie信息的持续登录用户表。

重新打开刚才使用的浏览器,访问项目首页并直接查看影片详情,会发现无需重新登录就可以直接访问。此时,再次查看数据库中表数据信息。Token更新过了

返回到浏览器首页,单击首页上方的用户“注销”连接,在Token有效期内进行用户手动注销。此时,再次查看数据库中表数据信息。

6.CSRF防护功能(下载项目并导入再做下面的功能

a.简介

跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS 利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。

CSRF攻击攻击原理及过程如下:

其中Web A为存在CSRF漏洞的网站,Web B为攻击者构建的恶意网站,User C为Web A网站的合法用户。

       1. 用户C打开浏览器,访问受信任网站A,输入用户名和密码请求登录网站A;

       2.在用户信息通过验证后,网站A产生Cookie信息并返回给浏览器,此时用户登录网站A成功,可以正常发送请求到网站A;

       3. 用户未退出网站A之前,在同一浏览器中,打开一个TAB页访问网站B;

       4. 网站B接收到用户请求后,返回一些攻击性代码,并发出一个请求要求访问第三方站点A;

       5. 浏览器在接收到这些攻击性代码后,根据网站B的请求,在用户不知情的情况下携带Cookie信息,向网站A发出请求。网站A并不知道该请求其实是由B发起的,所以会根据用户C的Cookie信息以C的权限处理该请求,导致来自网站B的恶意代码被执行。

参考:CSRF攻击与防御(写得非常好)_挺住我先走的博客-CSDN博客_csrf漏洞

CSRF攻击原理介绍和利用-腾讯云开发者社区-腾讯云

b.CSRF防护功能演示

创建数据修改页面

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>用户修改</title></head>
<body>
<div align="center">
    <form method="post" action="/updateUser">
        用户名: <input type="text" name="username" /><br />
        密  码: <input type="password" name="password" /><br />
        <button type="submit">修改</button>
    </form>
</div></body></html>

编写后台控制层方法,编写的toUpdate()方法用于向用户修改页面跳转,updateUser()方法用于对用户修改提交数据处理。其中,在updateUser()方法中只是演示了获取的请求参数,没有具体的业务实现。

@Controller
public class CSRFController {

    // 向用户修改页跳转
    @GetMapping("/toUpdate")
    public String toUpdate() {
        return "csrf/csrfTest";
    }

    // 用户修改提交处理
    @ResponseBody
    @PostMapping(value = "/updateUser")
    public String updateUser(@RequestParam String username, @RequestParam String password,
                             HttpServletRequest request) {

        System.out.println(username);
        System.out.println(password);
        String csrf_token = request.getParameter("_csrf");
        System.out.println(csrf_token);
        return "ok";
    }

}

重启chapter07项目,通过浏览器访问http://localhost:8080/toUpdate,

由于前面配置了请求拦截,会先被拦截跳转到用户登录页面。

在用户登录页面输入正确的用户信息后,就会自动跳转到用户修改页面。

数据修改请求中没有携带CSRF Token(CSRF令牌)相关的参数信息,所以被认为是不安全的请求。

整合Spring Security安全框架后,项目默认启用了CSRF安全防护功能,项目中所有涉及到数据修改方式的请求都会被拦截。

c.直接关闭CSRF功能

配置类SecurityConfig,在重写的configure(HttpSecurity http)方法中进行关闭配置即可

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable();
    ...
}

d.配置Security需要的CSRF Token

先开启CSRF再测试

Spring Security提供的CSRF Token配置,主要有:

针对Form表单数据修改的CSRF Token配置

<form method="post" action="/updateUser">
    <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/>
    用户名: <input type="text" name="username" /> <br />
    密  码: <input type="password" name="password" /> <br />
    <button type="submit">修改</button>
</form>

针对Ajax数据修改请求的CSRF Token配置(不做演示)

在页面<head>标签中添加<meta>子标签,并配置CSRF Token信息

<html>
    <head>
        <meta name="_csrf" th:content="${_csrf.token}"/>
        <meta name="_csrf_header" th:content="${_csrf.headerName}"/>
    </head>

在具体的Ajax请求中获取<meta>子标签中设置的CSRF Token信息并绑定在HTTP请求头中进行请求验证

$(function () {
    var token = $("meta[name='_csrf']").attr("content");
    var header = $("meta[name='_csrf_header']").attr("content");
    <!-- 在 Ajax 请求发送之前将CSRF Token信息绑定在HTTP请求头-->
    $(document).ajaxSend(function(e, xhr, options) {
        xhr.setRequestHeader(header, token);
    });
});

演示:添加jquery.min.js,如下图

修改csrfTest.html,重启项目后再次访问/toUpdate

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
    <head>
        <meta charset="UTF-8">
        <meta name="_csrf" th:content="${_csrf.token}" />
        <meta name="_csrf_header" th:content="${_csrf.headerName}" />
        <script th:src="@{/js/jquery.min.js}"></script>
        <script type="text/javascript">
            function loadXMLDoc(){
                $.ajax({
                    url:"/updateUser",
                    data:{username:"haha",password:"oooo"},
                    type:"post"
                });
            }

            $(function () {
                var token = $("meta[name='_csrf']").attr("content");
                var header = $("meta[name='_csrf_header']").attr("content");
                $(document).ajaxSend(function(e, xhr, options) {
                    xhr.setRequestHeader(header, token);
                });
            });
        </script>
        <title>用户修改</title>
    </head>
    <body>
        <button type="button" onclick="loadXMLDoc()">请求数据</button>
        <div align="center">
            <form method="post" action="/updateUser">
                <input type="hidden" th:name="${_csrf.parameterName}"
                th:value="${_csrf.token}" /> 
                用户名: <input type="text" name="username" /><br />
                密  码: <input type="password" name="password" /><br />
                <button type="submit">修改</button>
            </form>
        </div>
    </body>
</html>

六、Security管理前端页面

之前我们只是对前端页面做了权限控制,并没有做任何处理,用户体验差。所以我们使用security与thymeleaf整合实现前端页面的管理。

1.添加thymeleaf-extras-springsecurity5依赖启动器

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

2.修改前端页面,使用Security相关标签进行页面控制

在index.html页面中引入Security安全标签

<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
       xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">

页面顶部通过“xmlns:sec”引入了Security安全标签,页面中根据需要编写了4个<div>模块.

<div sec:authorize="isAnonymous()">
    <h2 align="center">
        游客您好,如果想查看电影<a th:href="@{/userLogin}">请登录</a>
    </h2>
</div>
<div sec:authorize="isAuthenticated()">
    <h2 align="center">
        <span sec:authentication="name" style="color: #007bff"></span>
        您好,您的用户权限为
        <span sec:authentication="principal.authorities"
                         style="color:darkkhaki"></span>,您有权观看以下电影
    </h2>
    <form th:action="@{/mylogout}" method="post">
        <input th:type="submit" th:value="注销" />
    </form>
</div>
<div sec:authorize="hasRole('common')">
    <h3>普通电影</h3>
    <ul>
        <li><a th:href="@{/detail/common/1}">我不是药神</a></li>
        <li><a th:href="@{/detail/common/2}">夏洛特烦恼</a></li>
    </ul>
</div>

<div sec:authorize="hasAuthority('ROLE_vip')">
    <h3>VIP专享</h3>
    <ul>
        <li><a th:href="@{/detail/vip/1}">速度与激情</a></li>
        <li><a th:href="@{/detail/vip/2}">猩球崛起</a></li>
    </ul>
</div>

3.效果测试

重启项目进行效果测试,项目启动成功后,通过浏览器访问http://localhost:8080/

作业:Security管理前端页面

  • 3
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值