Одна из серий Spring Security, краткое введение и реальный бой

Spring

Одна из серий Spring Security, краткое введение и реальный бой

Ввиду неравномерности весенних туториалов по безопасности на рынке, либо сразу анализируйте исходный код, как только придете, либо просто выкладывайте какой-то код и говорите, что можно так настроить, чтобы после реализации конкретных функций вы все еще немного разбираюсь в безопасности, и я также глубоко затронут ею, поэтому я полон решимости разработать серию весенней безопасности шаг за шагом, от поверхностного к глубокому.

глава

Одна из серий Spring Security, краткое введение и реальный бой

Анализ процесса аутентификации Spring Security Series II

Серия Spring Security, три пользовательских SMS-аутентификации при входе в систему

Spring Security Series 4 Разделение интерфейсных и серверных проектов использует jwt для аутентификации.

Spring Security Series Five: авторизация пользователей для проектов с разделением интерфейсов и серверов.

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.

Приведенный выше проект является одной из самых простых реализаций, конечно, он очень несовершенен и имеет несколько проблем:

  1. Если ничего не настроено, учетная запись и пароль генерируются определениями Spring Security. В реальном проекте учетная запись и пароль запрашиваются из базы данных.
  2. Интерфейс входа встроен. Если есть что проверить в процессе входа и на какую страницу перейти после успешного входа, это необходимо настроить.

Для первого вопроса нам нужно настроить логику проверки подлинности элемента управления, нужно только реализовать интерфейс 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 требует наследованияWebSecurityConfigurerAdapterclass, переопределите следующие три метода:

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"))

Суммировать

Процесс сертификации

Вышеупомянутый процесс является упрощенной версией, и в следующей статье будет подробно описан весь процесс аутентификации.

Ссылаться на:

Spring Security-Management Framework — Zhihu (zhihu.com)