用户认证(设置用户名、密码)

设置登录的用户名和密码

  • 第一种方式:通过配置文件
    修改application.properties,添加如下内容:
spring.security.user.name=antherd
spring.security.user.password=antherd
  • 第二种方式:配置类
package com.antherd.securitydemo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

    BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    String password = passwordEncoder.encode("123");
    auth.inMemoryAuthentication().withUser("lucy").password(password).roles("admin");
  }

  // PasswordEncoder: 配置密码加密类,否则运行报错 There is no PasswordEncoder mapped for the id "null"
  @Bean
  PasswordEncoder password() {
    return new BCryptPasswordEncoder();
  }
}
  • 第三种方式:自定义编写实现类
  1. 创建配置类,设置使用哪个userDetailService实现类
package com.antherd.securitydemo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
  
  @Autowired
  private UserDetailsService userDetailsService;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(password());
  }

  // PasswordEncoder: 配置密码加密类,否则运行报错 There is no PasswordEncoder mapped for the id "null"
  @Bean
  PasswordEncoder password() {
    return new BCryptPasswordEncoder();
  }
}
  1. 编写实现类,返回User对象,User对象有用户名密码和操作权限
package com.antherd.securitydemo.service;

import com.antherd.securitydemo.entity.Users;
import com.antherd.securitydemo.mapper.UsersMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    List<GrantedAuthority> auths = 
        AuthorityUtils.commaSeparatedStringToAuthorityList("admins");
    return new User("lucy", new BCryptPasswordEncoder().encode(123), auths);
  }
}

启动项目,访问:http://localhost:8111/test/hello,自动跳转到登陆页面,输入配置的用户名密码后,跳转到访问页面。

用户认证(查询数据库完成认证)

整合MyBatisPlus完成数据库操作

  1. 引入相关依赖
<!-- mybatis-plus -->
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.0.5</version>
</dependency>

<!-- mysql -->
<dependency>
  <groupId>mysql</groupId>
  <artifactId>mysql-connector-java</artifactId>
</dependency>

<!-- lombok 用来简化实体类 -->
<dependency>
  <groupId>org.projectlombok</groupId>
  <artifactId>lombok</artifactId>
</dependency>
  1. 创建数据库demo和数据库表
CREATE TABLE `users` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) DEFAULT NULL,
  `password` varchar(100) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

添加数据

INSERT INTO `demo`.`users` (`id`, `username`, `password`) VALUES (1, 'lucy', '123');
INSERT INTO `demo`.`users` (`id`, `username`, `password`) VALUES (2, 'mary', '456');
  1. 创建users表对应实体类
package com.antherd.securitydemo.entity;

import lombok.Data;

@Data
public class Users {

  private Integer id;

  private String username;

  private String password;
}
  1. 整合MybatisPlus,创建接口,继承map的接口
package com.antherd.securitydemo.mapper;

import com.antherd.securitydemo.entity.Users;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springframework.stereotype.Repository;

@Repository
public interface UsersMapper extends BaseMapper<Users> {

}
  1. 在UserDetailsService调用mapper里面的方法查询数据库进行用户认证
package com.antherd.securitydemo.service;

import com.antherd.securitydemo.entity.Users;
import com.antherd.securitydemo.mapper.UsersMapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {

  @Autowired
  private UsersMapper usersMapper;

  @Override
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 调用usersMapper方法,根据用户名查询数据库
    QueryWrapper<Users> wrapper = new QueryWrapper<>();
    // where username = ?
    wrapper.eq("username", username);
    Users users = usersMapper.selectOne(wrapper);
    // 判断
    if (users == null) { // 数据库没有用户名, 认证失败
      throw new UsernameNotFoundException("用户名不存在!");
    }
    List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
    // 从查询数据库返回users对象,得到用户名和密码,返回
    return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()), auths);
  }
}
  1. 在启动类添加注解MapperScan
@MapperScan("com.antherd.securitydemo.mapper")
  1. 配置文件中配置数据库信息
# mysql数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456
  1. 启动测试
用户认证(自定义用户登录页面)
  1. 在配置类实现相关的配置
package com.antherd.securitydemo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {

  @Autowired
  private UserDetailsService userDetailsService;

  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService).passwordEncoder(password());
  }

  // PasswordEncoder: 配置密码加密类,否则运行报错 There is no PasswordEncoder mapped for the id "null"
  @Bean
  PasswordEncoder password() {
    return new BCryptPasswordEncoder();
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() // 自定义自己编写的登录页面
        .loginPage("/login.html") // 登录页面设置
        .loginProcessingUrl("/user/login") //登录访问路径
        .defaultSuccessUrl("/test/index").permitAll() // 登录成功后,跳转路径
        .and().authorizeRequests() //
          .antMatchers("/", "/test/hello", "/user/login").permitAll() // 设置哪些路径可以直接访问,不需要认证
        .anyRequest().authenticated()
        .and().csrf().disable(); // 关闭csrf防护

  }
}
  1. 创建相关页面,controller
    在resource文件夹下创建static文件夹,添加文件login.html,用户名、密码参数必须为:username、password
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>title</title>
</head>
<body>
  <form action="/user/login" method="post">
    用户名:<input type="text" name="username">
    <br/>
    密码:<input type="text" name="password">
    <br/>
    <input type="submit" value="login" />
  </form>
</body>
</html>

在TestController中添加接口

@GetMapping("/index")
public String index() {
  return "hello index";
}
  1. 启动测试
    访问:http://localhost:8111/test/hello 不需要认证
    访问:http://localhost:8111/test/index 跳转到自定义认证页面
用户认证(基于权限访问控制)
  • ① hasAuthority方法:如果当前的主体有指定的权限,则返回true,否则返回false
  1. 在配置类设置当前访问地址有哪些权限
// 当前登录用户,只有具有admins权限才可以访问这个路径
.antMatchers("/test/index").hasAuthority("admins")
  1. 在UserDetailsService,把返回User对象设置权限
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("admins"); // 将admins改成123,返回登录访问403
  • ② hasAnyAuthority方法:如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true
.antMatchers("/test/index").hasAnyAuthority("admins, manager")
  • ③ hasRole方法:如果用户具备给定角色就允许访问,否则出现403。如果当前主体具有指定的角色,则返回true
    查看源码
return "hasRole('ROLE_" + role + "')";

MyUserDetailsService

AuthorityUtils.commaSeparatedStringToAuthorityList("admins, ROLE_sale"); // 角色权限需要加上"ROLE_"前缀

SecurityConfigTest

.antMatchers("/test/index").hasRole("sale")
  • ④ hasAnyRole方法:表示用户具备任何一个条件都可以访问
    MyUserDetailsService
AuthorityUtils.commaSeparatedStringToAuthorityList("admins, ROLE_admin, ROLE_sale");

SecurityConifgTest

.antMatchers("/test/index").hasAnyRole("sale")
用户认证(自定义403页面)

MyUserDetailsService

AuthorityUtils.commaSeparatedStringToAuthorityList("admins, ROLE_admin, ROLE_sale1`");

ResourceServerConfigurerAdapter 设置请求权限 resource有哪些权限_安全框架


在resource/static中新建页面 unauth.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>title</title>
</head>
<body>
  <h1>没有访问权限!</h1>
</body>
</html>

在配置类中配置即可,SecurityConfigTest.configure,添加如下内容:

// 配置没有权限访问跳转自定义页面
http.exceptionHandling().accessDeniedPage("/unauth.html");
用户授权(注解使用)
  • ① Secured:判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀"ROLE_"
  1. 启动类(配置类)开启注解
@EnableGlobalMethodSecurity(securedEnabled = true)
  1. 在controller的方法上面使用注解,设置角色
@GetMapping("/update")
@Secured({"ROLE_sale", "ROLE_manager"})
public String update() {
	return "hello update";
}
  1. userDetailService设置用户角色
AuthorityUtils.commaSeparatedStringToAuthorityList("admins, ROLE_admin, ROLE_sale");
  • ② PreAuthorize:注解适合进入方法前的权限校验,可以将登陆用户的roles/permissions参数传到方法中
  1. 启动类(配置类)开启注解
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
  1. 在controller的方法上面使用注解,设置角色
@GetMapping("/update")
@PreAuthorize("hasAnyAuthority('admins')")
public String update() {
  return "hello update";
}
  1. userDetailService设置用户角色
AuthorityUtils.commaSeparatedStringToAuthorityList("admins, ROLE_admin, ROLE_sale");
  • ③ PostAuthority:注解使用并不多,在方法执行后再进行权限校验,适合验证带有返回值的权限
    使用此注解先要在启动类上开启注解功能
  1. 启动类(配置类)开启注解
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
  1. 在controller的方法上面使用注解,设置角色
@GetMapping("/update")
@PreAuthorize("hasAnyAuthority('admins')")
public String update() {
  return "hello update";
}
  1. userDetailService设置用户角色,admin,controller中权限为admins
AuthorityUtils.commaSeparatedStringToAuthorityList("admin, ROLE_admin, ROLE_sale");
  1. 访问:http://localhost:8111/test/update ,返回403页面,但是方法执行了
  • ④ PreFilter:传入方法数据进行过滤
  • ⑤ PostFilter:方法返回数据进行过滤
AuthorityUtils.commaSeparatedStringToAuthorityList("admins, ROLE_admin, ROLE_sale");
@GetMapping("/getAll")
@PostAuthorize("hasAnyAuthority('admins')")
@PostFilter("filterObject.username=='admin1'")
public List<Users> getAllUser() {
  ArrayList<Users> list = new ArrayList<>();
  list.add(new Users(11, "admin1", "6666"));
  list.add(new Users(21, "admin2", "8888"));
  System.out.println(list);
  return list;
}

访问:http://localhost:8111/test/getAll ,看一下返回值

用户注销
  1. 在配置类中添加退出的配置
// 退出
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
  1. 测试:
    ① 修改配置类,登录成功之后跳转到成功页面
.defaultSuccessUrl("/success.html").permitAll() // 登录成功后,跳转路径

② 在成功页面添加超链接,写设置的退出路径

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Title</title>
</head>
<body>
  登录成功!
  <a href="/logout">退出</a>
</body>
</html>

③ 登录成功后,在点击页面点击退出

访问:http://localhost:8111/login.html、然后新开标签页访问 http://localhost:8111/login.html,可以访问,在点击退出按钮,在访问 http://localhost:8111/login.html,发现不能访问

自动登录

安全框架机制实现自动登录

一、实现原理

ResourceServerConfigurerAdapter 设置请求权限 resource有哪些权限_spring security_02


二、具体实现

  1. 创建数据库表(可以自动生成)
    JdbcTokenRepositoryImpl.CREATE_TABLE_SQL
CREATE TABLE persistent_logins (
username VARCHAR ( 64 ) NOT NULL,
series VARCHAR ( 64 ) PRIMARY KEY,
token VARCHAR ( 64 ) NOT NULL,
last_used TIMESTAMP NOT NULL)
  1. 配置类,注入数据源,配置操作数据库对象
// 注入数据源
@Autowired
private DataSource dataSource;

@Bean
public PersistentTokenRepository persistentTokenRepository() {
  JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
  jdbcTokenRepository.setDataSource(dataSource);
  // jdbcTokenRepository.setCreateTableOnStartup(true); // 自动创建表
  return jdbcTokenRepository;
}
  1. 配置类中配置自动登录
.and().rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60) // 设置有效时长,单位秒
.userDetailsService(userDetailsService)
  1. 在登录页面中添加复选框
<input type="checkbox" name="remember-me" />自动登录
<br />
  1. 访问 http://localhost:8111/login.html 浏览器中存入了cookies,并且数据库中记录了相关信息


    关闭浏览器后,重启打开,访问 http://localhost:8111/login.html 发现登录任然有效
CSRF

跨站请求伪造(英语:Cross-site request forgery)
从Spring Security 4.0 开始,默认情况下会启用CSRF保护,以防止CSRF攻击应用,Spring Security CSRF会针对PATCH、POST、PUT 和 DELETE 方法进行防护

ResourceServerConfigurerAdapter 设置请求权限 resource有哪些权限_spring security_03


在pom文件中引入如下依赖

<!-- 对Thymeleaf添加Spring Security标签支持 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

在templates目录下新建三个页面
csrf/csrf_token.html

<!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>
    <span th:text="${_csrf.token}"></span>
  </div>
</body>
</html>

csrf/csrfTest.html

<!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="update_token">
      <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>

login/login.html

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>title</title>
</head>
<body>
<form action="/user/login" method="post">
  用户名:<input type="text" name="username">
  <br/>
  密码:<input type="text" name="password">
  <br/>
  <input type="checkbox" name="remember-me" />自动登录
  <br />
  <input type="submit" value="login" />
</form>
</body>
</html>

新建 CSRFUserDetailsService

package com.antherd.securitydemo.service;

import java.util.ArrayList;
import java.util.List;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

@Service("userDetailsService")
public class CSRFUserDetailsService implements UserDetailsService {

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

    List<SimpleGrantedAuthority> list = new ArrayList<>();
    list.add(new SimpleGrantedAuthority("role"));
    UserDetails userDetails = new User("lucy", new BCryptPasswordEncoder().encode("123"),
        list);
    return userDetails;
  }
}

SecurityConfigCsrf

package com.antherd.securitydemo.config;

import org.springframework.beans.factory.annotation.Autowired;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfigCsrf extends WebSecurityConfigurerAdapter {

  @Autowired
  private UserDetailsService userDetailsService;

  // 实现用户身份认证
  @Override
  protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
    auth.userDetailsService(userDetailsService).passwordEncoder(encoder);
  }

  @Override
  protected void configure(HttpSecurity http) throws Exception {
    // 配置url的访问权限
    http.authorizeRequests()
        .antMatchers("/").permitAll()
        .antMatchers("/**update**").permitAll()
        .antMatchers("/login/**").permitAll()
        .anyRequest().authenticated();

    // 关闭csrf保护功能
//    http.csrf().disable();

    // 使用自定义的登录窗口
    http.formLogin()
        .loginPage("/userLogin").permitAll()
        .usernameParameter("username").passwordParameter("password")
        .defaultSuccessUrl("/")
        .failureUrl("/userLogin?error");
  }
}

CSRFController

package com.antherd.securitydemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
public class CSRFController {

  @GetMapping("/toupdate")
  public String test(Model model) {
      return "csrf/csrfTest";
  }

  @PostMapping("/update_token")
  public String getToken() {
    return "csrf/csrf_token";
  }
}

LoginController

package com.antherd.securitydemo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {

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

现将 csrfTest页面中如下内容注释掉

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

启动测试,访问 http://localhost:8111/toupdate , 发现登录触发CSRF,跳转到 /userLogin?error 页面
取消上面注释后,在启动测试,可以正常登录