Одна из серий Spring Security, краткое введение и реальный бой
Ввиду неравномерности весенних туториалов по безопасности на рынке, либо сразу анализируйте исходный код, как только придете, либо просто выкладывайте какой-то код и говорите, что можно так настроить, чтобы после реализации конкретных функций вы все еще немного разбираюсь в безопасности, и я также глубоко затронут ею, поэтому я полон решимости разработать серию весенней безопасности шаг за шагом, от поверхностного к глубокому.
глава
Одна из серий Spring Security, краткое введение и реальный бой
Анализ процесса аутентификации Spring Security Series II
Серия Spring Security, три пользовательских SMS-аутентификации при входе в систему
Spring Security Series Six Анализ процесса авторизации
Вышеизложенное является главой моего собственного плана, который может измениться, а может и не измениться: сначала выкопайте яму, а затем медленно засыпайте ее.
Spring Security — это инфраструктура безопасности, обеспечивающая декларативную защиту для приложений на базе Spring. Spring Security предоставляет полное решение для обеспечения безопасности, включая аутентификацию пользователя ( Authentication ) и права пользователя ( Authorization ) из двух частей. Аутентификация пользователя заключается в подтверждении того, имеет ли пользователь право входа в систему, как правило, используется аутентификация по имени пользователя и паролю, то есть логин. Разрешения пользователей определяют, какие пользователи могут получить доступ к каким ресурсам.
Сценарии применения
Существует много причин для использования Spring Security, большинство из которых связано с отсутствием функций, связанных с безопасностью, в спецификации JavaEE.В то же время функции, связанные с безопасностью, необходимо перенести в другой набор приложений, что также требует много работы, которую нужно перенастроить, и Spring Security решает проблему.Для решения этих проблем он предоставляет множество полезных, настраиваемых функций безопасности.
настоящий бой
Давайте посмотрим на самую базовую демонстрацию безопасности, это все в 2021. Конечно, проект должен быть собран с использованием springboot.
Чтобы использовать безопасность в springboot, вам нужно только ввести соответствующие стартовые зависимости
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Затем сначала настройте имя пользователя и пароль в файле конфигурации:
server:
port: 8080
spring:
security:
user:
name: user
password: 123456
Создайте новый файл index.html для проверки входа в систему и доступа к ней:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hello</title>
</head>
<body>
<p>hello spring security</p>
</body>
</html>
Запустите проект, посетитеhttp://localhost:8080, веснаБезопасность вступила в силу, и все запросы перехватываются по умолчанию.В это время, если нет входа в систему, он перейдет на встроенную страницу входа:
После ввода учетной записи и пароля он перейдет к index.html.
Приведенный выше проект является одной из самых простых реализаций, конечно, он очень несовершенен и имеет несколько проблем:
- Если ничего не настроено, учетная запись и пароль генерируются определениями Spring Security. В реальном проекте учетная запись и пароль запрашиваются из базы данных.
- Интерфейс входа встроен. Если есть что проверить в процессе входа и на какую страницу перейти после успешного входа, это необходимо настроить.
Для первого вопроса нам нужно настроить логику проверки подлинности элемента управления, нужно только реализовать интерфейс UserDetailsService.
Интерфейс определяется следующим образом:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
Реализуем этот метод интерфейса.Имя пользователя передается из фронтенда.Нам нужно найти пользователя в БД и упаковать пользователя какUserDetails
объект вернулся вsecurity
Вот и все.
UserDetails
Это также интерфейс, который определяет информацию, связанную с пользователем:
public interface UserDetails extends Serializable {
/**
* 返回授予用户的权限,不能为空
* @return the authorities, sorted by natural key (never <code>null</code>)
*/
Collection<? extends GrantedAuthority> getAuthorities();
/**
* 用户的密码
* @return the password
*/
String getPassword();
/**
* 用户的用户名
* @return the username (never <code>null</code>)
*/
String getUsername();
/**
* 用户的帐户是否已过期。过期的帐户无法通过身份验证
*/
boolean isAccountNonExpired();
/**
* 用户是锁定还是解锁。锁定的用户无法通过身份验证
* @return 没有锁定返回true
*/
boolean isAccountNonLocked();
/**
* 用户的凭据(密码)是否已过期。过期的凭据会阻止身份验证
*/
boolean isCredentialsNonExpired();
/**
* 启用还是禁用用户。禁用的用户无法通过身份验证
*/
boolean isEnabled();
}
Этот интерфейс имеет два класса реализации:
Класс User просто определяет некоторые свойства черепахи, которые соответствуют методам в UserDetails:
public class User implements UserDetails, CredentialsContainer {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private static final Log logger = LogFactory.getLog(User.class);
private String password;
private final String username;
private final Set<GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
public User(String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"Cannot pass null or empty values to constructor");
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
}
}
//...省略
Класс MutableUser — это класс-оболочка, который включает атрибут пароля, который используется для переноса модификации пароля пользователя:
class MutableUser implements MutableUserDetails {
private String password;
private final UserDetails delegate;
MutableUser(UserDetails user) {
this.delegate = user;
this.password = user.getPassword();
}
}
//...省略
Вышеупомянутая реализация userdetail часто не соответствует нашим потребностям, поэтому нам обычно нужно настроить ееUserDetails
класс реализации.
шифрование пароля
Вышеизложенное заключается в том, чтобы узнать пользователя из базы данных, но пароль пользователя не сравнивается, поэтому должен быть процесс анализа и сравнения паролей.
В Spring Security в контейнере должен быть экземпляр PasswordEncoder (независимо от того, выполняется ли соответствие пароля клиента и пароля базы данных Spring Security, а в Security нет анализатора паролей по умолчанию). Поэтому при настройке логики входа в контейнер необходимо внедрить bean-объект PaswordEncoder.
Интерфейс PasswordEncoder определяется следующим образом:
public interface PasswordEncoder {
/**
* 编码原始密码。通常,良好的编码算法将SHA-1或更大的哈希值与8字节或更大的随机生成的盐结合使用
*/
String encode(CharSequence rawPassword);
/**
* 验证从存储中获取的编码密码,也对提交的原始密码进行编码。如果密码匹配,则返回true;否则,返回false。存储的 * 密码本身不会被解码。
*/
boolean matches(CharSequence rawPassword, String encodedPassword);
/**
* 如果需要再次对编码后的密码进行编码以提高安全性,则返回true,否则返回false。默认实现始终返回false
*/
default boolean upgradeEncoding(String encodedPassword) {
return false;
}
}
В безопасность встроено множество парсеров:
в,BCryptPasswordEncoder
Этот синтаксический анализатор, официально рекомендованный Spring Security, представляет собой конкретную реализацию сильного метода хеширования bcrypt и одностороннего шифрования на основе алгоритма Hash. Стойкость шифрования можно контролировать с помощью силы, по умолчанию 10, чем больше длина, тем выше безопасность.
Bcrypt имеет две особенности:
- Значение каждого HASH отличается
- Расчет идет очень медленно
Следовательно, после использования Bcrypt для шифрования стоимость взлома пароля злоумышленником становится неприемлемой, но цена заключается в том, что производительность самого приложения также будет затронута, но поведение при входе в систему не происходит в любое время, поэтому его можно терпеть. .
Он также очень прост в использовании:
@SpringBootTest
@Slf4j
class SecurityApplicationTests {
@Test
void testPasswordEncoder(){
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String encode = encoder.encode("123456");
log.info("编码后的密码:{}, 密码是否正确:{}",encode,encoder.matches("123456",encode));
}
}
Следует отметить, что Spring Security требует наличия экземпляра PasswordEncoder в контейнере при настройке логики входа в систему. Поэтому вам нужно написать класс конфигурации, чтобы сначала внедрить парсер паролей:
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
пользовательский логин
UserDetailsService и PasswordEncoder, описанные выше, необходимо использовать при настройке логики входа в систему.Для входа в систему сначала нам нужно настроить дизайн прототипа базы данных.
структура таблицы базы данных
Таблица базы данных разработана в соответствии с идеей RBAC, а следующая диаграмма ER:
Скрипт базы данных:
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `power`;
CREATE TABLE `power` (
`id` int NOT NULL AUTO_INCREMENT,
`title` varchar(32) NOT NULL,
`url` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='权限';
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int NOT NULL AUTO_INCREMENT,
`role_name` varchar(32) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `role` VALUES ('1', 'admin');
INSERT INTO `role` VALUES ('2', 'normal_user');
DROP TABLE IF EXISTS `role_power`;
CREATE TABLE `role_power` (
`id` int NOT NULL AUTO_INCREMENT,
`role_id` int NOT NULL,
`power_id` int NOT NULL,
PRIMARY KEY (`id`),
KEY `role_power___fk_power_id` (`power_id`),
KEY `role_power___fk_role_id` (`role_id`),
CONSTRAINT `role_power___fk_power_id` FOREIGN KEY (`power_id`) REFERENCES `power` (`id`),
CONSTRAINT `role_power___fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(64) NOT NULL,
`password` varchar(64) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `user` VALUES ('1', 'test1', '$2a$10$pjHyw9MSGC/i6k546Ii/0uLFgTK4WYB4.8bSRq7yB4dy.ZpBLxOha');
INSERT INTO `user` VALUES ('2', 'test2', '$2a$10$pjHyw9MSGC/i6k546Ii/0uLFgTK4WYB4.8bSRq7yB4dy.ZpBLxOha');
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int NOT NULL AUTO_INCREMENT,
`user_id` int NOT NULL,
`role_id` int NOT NULL,
UNIQUE KEY `user_role_pk` (`id`),
KEY `user_role___fk_role_id` (`role_id`),
KEY `user_role___fk_user_id` (`user_id`),
CONSTRAINT `user_role___fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `role` (`id`),
CONSTRAINT `user_role___fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `user` (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `user_role` VALUES ('1', '1', '1');
INSERT INTO `user_role` VALUES ('2', '1', '2');
INSERT INTO `user_role` VALUES ('3', '2', '2');
настроить mybatis
Добавьте зависимости:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
Создайте новый пользовательский класс и реализуйте интерфейс UserDetails:
public class User implements UserDetails {
private Integer id;
private String username;
private String password;
private List<Role> roleList;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return roleList.stream().map(role ->
new SimpleGrantedAuthority(role.getRoleName())).collect(Collectors.toList());
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
Создайте новый маппер:
public interface UserDao {
User queryByName(String name);
}
Соответствующий xml:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.lin.security.dao.UserDao">
<resultMap type="com.lin.security.entity.User" id="UserMap">
<result property="id" column="id" jdbcType="INTEGER"/>
<result property="username" column="username" jdbcType="VARCHAR"/>
<result property="password" column="password" jdbcType="VARCHAR"/>
<collection property="roleList" ofType="com.lin.security.entity.Role">
<result property="id" column="rid" jdbcType="INTEGER"/>
<result property="roleName" column="role_name" jdbcType="VARCHAR"/>
</collection>
</resultMap>
<select id="queryByName" parameterType="java.lang.String" resultMap="UserMap">
select u.*, r.id as rid, r.role_name
from security.user u
left join user_role ur on u.id = ur.user_id
inner join role r on ur.role_id = r.id
where username = #{name}
</select>
</mapper>
Настройте сервисную логику входа и реализуйте интерфейс UserService:
@Service("userService")
@Slf4j
public class UserServiceImpl implements UserService {
@Resource
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) {
User user = userDao.queryByName(username);
if (user == null){
throw new UsernameNotFoundException("用户名错误");
}
return user;
}
}
Нам нужно только запросить пользователя здесь и передать проверку пароля в службу безопасности.
пользовательская главная страница
Первая проблема решена, давайте посмотрим на вторую проблему.
Мы создаем три новые страницы: страницу входа, страницу, которая переходит после успешного входа в систему, и страницу, отображаемую при сбое входа.
login.html:
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>login</title>
</head>
<body>
<form action="/login" method="post">
<input type="text" name="username"/>
<input type="password" name="password"/>
<input type="submit" value="提交"/>
</form>
</body>
</html>
index.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>hello</title>
</head>
<body>
<div>
<p>hello spring security</p>
<p>用户名:<span th:text="${user.username}"></span></p>
</div>
</body>
</html>
failure.html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>failure</title>
</head>
<body>
<div>
<p>用户民或密码错误</p>
</div>
</body>
</html>
конфигурация безопасности
Настройка безопасности Spring требует наследованияWebSecurityConfigurerAdapter
class, переопределите следующие три метода:
protected void configure(AuthenticationManagerBuilder auth) throws Exception {}
public void configure(WebSecurity web) throws Exception {}
protected void configure(HttpSecurity httpSecurity) throws Exception {}
в,AuthenticationManagerBuilder
Информация, используемая для настройки глобальной аутентификации, — это UserDetailsService и AuthenticationProvider.
WebSecurity
Используется для настройки правил игнорирования глобальных запросов, таких как некоторые статические файлы, выпуск страницы входа в систему.
HttpSecurity
Для конкретной конфигурации правила управления разрешениями нам нужно только переписать этот метод здесь.
Измените класс конфигурации:
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
//super.configure(http);
http.formLogin()
.loginPage("/login") //登录页面
.successForwardUrl("/index") //登录成功后的页面
.failureForwardUrl("/failure") //登录失败后的页面
.and()
// 设置URL的授权
.authorizeRequests()
// 这里需要将登录页面放行
.antMatchers("/login")
.permitAll()
//除了上面,其他所有请求必须被认证
.anyRequest()
.authenticated()
.and()
// 关闭csrf
.csrf().disable();
}
@Bean
public PasswordEncoder getPasswordEncoder(){
return new BCryptPasswordEncoder();
}
}
HttpSecurity
Есть много других методов, вот некоторые из них:
метод | иллюстрировать |
---|---|
formLogin() | Включить проверку подлинности формы |
loginPage() | Укажите страницу входа |
successForwardUrl() | Укажите страницу для перехода после успешного входа в систему |
failureForwardUrl() | Укажите страницу для перехода после неудачного входа в систему |
authorizeRequests() | Включить ограничения доступа для запросов, сделанных с помощью HttpServletRequest. |
oauth2Login() | Включить аутентификацию oauth2 |
rememberMe() | включи记住我 Проверка (с использованием файлов cookie) |
addFilter() | Добавить пользовательский фильтр |
csrf() | Включить поддержку csrf |
Далее пишем контроллер:
@Controller
public class UserController {
@RequestMapping("/login")
public String login(){
return "login";
}
@RequestMapping("/index")
public String index(ModelMap modelMap){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
User principal = (User) authentication.getPrincipal();
modelMap.put("user",principal);
return "index";
}
@RequestMapping("/failure")
public String failure(){
return "failure";
}
}
контрольная работа
запустить проект, посетитьhttp://localhost:8080, он перейдет прямо на страницу входа, введет пароль учетной записи и перейдет на страницу, где вход выполнен успешно.
Другая конфигурация
Если вам нужно сделать какие-то другие вещи после успешного или неудачного входа в систему, то приведенный выше код не может удовлетворить это требование.Вам необходимо настроить логику успешного/неудачного входа.Мы можем изменить ее в файле конфигурации:
.successHandler((httpServletRequest, httpServletResponse, authentication) -> httpServletResponse.sendRedirect("/index"))
.failureHandler((httpServletRequest, httpServletResponse, authentication) -> httpServletResponse.sendRedirect("/failure"))
Суммировать
Процесс сертификации
Вышеупомянутый процесс является упрощенной версией, и в следующей статье будет подробно описан весь процесс аутентификации.
Ссылаться на: