7. Работа со Spring Security, часть 1

Spring Security – это фреймворк обеспечения безопасности, предоставляющий возможность декларативного управления безопасностью приложений на основе фреймворка Spring.

Создадим новый проект, который включает модуль Spring Security или добавим в существующий проект зависимость

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

При попытке перейти на любой URL-адрес проекта нас перенаправит на форму ввода логина и пароля

По умолчанию, логином является user, а пароль генерируется каждый раз при старте приложения.

Если вы ввели правильно логин и пароль, то сервер переадресует вас на указанный URL.

В файле application.properties вы можете указать желаемый логин и пароль для пользователя по умолчанию (в данной лекции будет использоваться конфигурация с помощью языка yaml).

resources/application.yml
spring:
  security:
    user:
      name: nick
      password: 1234

Фреймворк Spring Security "из коробки" предоставляет вам возможность простой версии так называемой form-based аутентификации. Если быть точнее, то по умолчанию, Spring Security реализует следующее поведение:

  • добавляет обязательный процесс аутентификации для всех URL;

  • добавляет форму для входа;

  • обрабатывает ошибки формы ввода;

  • создает пользователя по умолчанию и генерирует пароль.

Основные понятия, связанные со Spring Security:

Authentication

Authorization

Principal - текущий залогиненный пользователь или текущий залогиненный аккаунт (если у одного физического лица или программы есть несколько аккаунтов, то тогда ему будет соответствовать несколько возможных principal`ов). Иногда, в общем случае, principal - это субъект, который принимает участие в осуществлении процедур безопасности. В качестве principal могут выступать люди, компьютеры, службы, процессы или их группа;

Granted Authority - ;

Role - .

Настройка процесса аутентификации в Spring

Для того, чтобы сконфигурировать процесс аутентификации, необходимо создать объект AuthenticationManager, в котором следует указать требуемые параметры аутентификации. Объект типа AuthenticationManager обычно настраивают с помощью builder`а, который имеет тип AuthenticationManagerBuilder.

Добавим класс SecurityConfig, который наследуется от класса WebSecurityConfigurerAdaper. Также укажем аннотации @Configuration (это означает, что данный класс является конфигурационным) и @EnableWebSecurity (это означает, что данный класс является содержит настройки для защиты веб-приложения).

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
}

Переопределим метод configure(), который принимает на вход объект типа AuthenticationManagerBuilder (обратите внимание, что нам нужна именно эта версия перегруженного метода).

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        super.configure(auth);
    }
}

Для начала укажем, что источник аутентификации это жестко прописанные пользователи (так называемая inMemoryAuthentication(). Далее указываем логин, пароль и роль пользователя.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("foo")
                .password("bar")
                .roles("USER");
    }
}

Если необходимо указать несколько пользователей, после параметров первого пользователя вызываете метод and() после чего указываете параметры следующего пользователя.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("foo")
                .password("bar")
                .roles("USER")
                .and()
                .withUser("nick")
                .roles("MODERATOR");
    }
}

Хеширование паролей

Хранить пароли без хеширования является грубейшим нарушением правил безопасности, поэтому нам необходимо добавить процесс хеширования пароля в нашу систему.

Не будем вдаваться в подробности различных алгоритмов хеширования пароля, просто скажем, что на даннай момент рекомендуемым является алгоритм Bcrypt. Для обеспечения хеширования, вы можете поступить несколькими способами.

Первый способ - создайте Bean, который будет возвращать объект Encoder`а и добавьте его как метод конфигурационного класса.

Далее найдите в интернете генератор хеша с помощью алгоритма Bcrypt, скопируйте хеш для вашего пароля в метод password.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("foo")
                .password("$2y$12$kElDOfhm4WgdsDc4UQjgtuz0VEi5MOqVVhXaMoD1F2lhLivokhCqe")
                .roles("USER")
                .and()
                .withUser("nick")
                .roles("MODERATOR");
    }

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Если не хотите использовать Bean для хеширования пароля, можете в начале хеша добавить обозначение, что это хеш для алгоритма bcrypt.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("foo")
                .password("{bcrypt}$2y$12$.JzV1A3qlof1.NzZpGaTYO1b26JGHevg0900QvwrSOdU3U9.g4hta")
                .roles("USER");
    }

//    @Bean
//    public PasswordEncoder getPasswordEncoder() {
//        return new BCryptPasswordEncoder();
//    }
}

Настройка процесса авторизации

Добавим класс контроллера

@Controller
public class IndexController {

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

    @GetMapping("/user/index")
    public String user_index() {
        return "/user/index";
    }

    @GetMapping("/admin/index")
    public String admin_index() {
        return "/admin/index";
    }
}

Изменим SecurityConfig

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("foo")
                .password("$2y$12$kElDOfhm4WgdsDc4UQjgtuz0VEi5MOqVVhXaMoD1F2lhLivokhCqe")
                .roles("USER")
                .and()
                .withUser("admin")
                .password("$2y$12$DQlTV6V1wMKEoCIW5lo1huAn2/bRk4hULDmRS5Jw6YW7HayHV4K66")
                .roles("ADMIN");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/", "static/css", "static/js").permitAll()
                .antMatchers("/user/**").hasAnyRole("USER", "ADMIN")
                .antMatchers("/admin/**").hasRole("ADMIN")
                .and().formLogin();
    }

    @Bean
    public PasswordEncoder getPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

Изменим formLogin() на httpBasic().

Создадим MyUserDetailsService

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    UserRepository repository;

    @Override
    public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
        return new MyUserDetails(s);
    }
}

Создадим MyUserDetails

public class MyUserDetails implements UserDetails {
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"));
    }

    private String userName;

    public MyUserDetails(String userName) {
        this.userName = userName;
    }

    public MyUserDetails() {
    }

    @Override
    public String getPassword() {
        return "$2y$12$8fKhxW71f4DzkzXYCh592.I.cd1uKkMrNwrHAApR1x5KHJ3qy1IjS";
    }

    @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;
    }
}

Добавим в pom.xml

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

<!-- PostgreSQL -->
<dependency>
    <groupId>org.postgresql</groupId>
    <artifactId>postgresql</artifactId>
</dependency>

Настроим подключение к БД

application.yml
  jpa:
    database: POSTGRESQL
    show-sql: true
    hibernate:
      ddl-auto: create-drop
    properties:
      hibernate:
        dialect: org.hibernate.dialect.PostgreSQLDialect
  datasource:
    platform: postgres
    url: jdbc:postgresql://localhost:5432/ejournal
    username: ejournal_user
    password: 123456
    driverClassName: org.postgresql.Driver

Добавим сущность User

@Entity
@Table(name = "profile")
@Data
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(name = "user_name")
    private String userName;
    private String password;
    private boolean active;
    private String roles;
}

Изменим MyUserDetailsService

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    UserRepository repository;

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        Optional<User> user = repository.findByUserName(s);

        user.orElseThrow(() -> new UsernameNotFoundException("User not found: " + s));

        return user.map(MyUserDetails::new).get();
    }
}

Создадим UserRepository

public interface UserRepository extends JpaRepository<User, Integer> {

    Optional<User> findByUserName(String userName);
}

Изменим MyUserDetails

public class MyUserDetails implements UserDetails {
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorityList;
    }

    private String userName;
    private String password;
    private boolean active;
    private List<GrantedAuthority> authorityList;

    public MyUserDetails(User userName) {
        this.userName = userName.getUserName();
        this.password = userName.getPassword();
        this.active = userName.isActive();
        this.authorityList = Arrays.stream(userName.getRoles().split(",")).map(SimpleGrantedAuthority::new).collect(Collectors.toList());
    }

    public MyUserDetails() {
    }

    @Override
    public String getPassword() {
        return password;
    }

    @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 active;
    }
}

Last updated