Spring Boot + Spring Security + Простой учебник Thymeleaf

Spring Boot

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

Адрес гитхаба:GitHub.com/Smith-C Швейцария….

Оригинальный адрес:woohoo.in lighting.org/archives/tickets….

Если у вас есть какие-либо вопросы, опубликуйте вопрос на GitHub, и я отвечу, когда у меня будет время.

Этот проект основан на Spring Boot 2 + Spring Security 5 + Thymeleaf 2 + JDK11 (вы также можете использовать 8, разница должна быть небольшой)

Реализованы следующие функции:

  • Управление разрешениями на основе аннотаций
  • Использование тегов Spring Security в Thymeleaf
  • Пользовательская аннотация разрешения
  • Помните функцию пароля

Если вам нужен учебник по созданию инфраструктуры безопасности с отдельными интерфейсом и сервером, вы можете обратиться к:Spring Boot 2 + Spring Security 5 + Одностраничное приложение JWT Restful Solution

Демонстрация проекта

Если вы хотите испытать это непосредственно, непосредственноcloneпроект, запуститьmvn spring-boot:runДоступ к команде можно получить, а к правилам URL можно получить доступ после обучения.

титульная страница

首页

авторизоваться

登入

выход

登出

Домашняя страница

登出

Страница администратора

登出

403 Неавторизованная страница

登出

Основы безопасности Spring

Цепочка фильтров Spring Security

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

  1. ....classНекоторые пользовательские фильтры (вы можете выбрать вставить перед каким фильтром при настройке), потому что это требование варьируется от человека к человеку, в этой статье это не будет обсуждаться, вы можете изучить его самостоятельно
  2. UsernamePasswordAithenticationFilter.classФильтр проверки входа в форму, поставляемый с Spring Security, также является основным фильтром, используемым в этой статье.
  3. BasicAuthenticationFilter.class
  4. ExceptionTranslation.classинтерпретатор исключений
  5. FilterSecurityInterceptor.classПерехватчик наконец-то решил, может ли запрос пройти
  6. ControllerПоследний контроллер, который мы написали сами

Описание связанного класса

  • User.class: Обратите внимание, что этот класс написан не нами, а официально предоставлен Spring Security. Он предоставляет некоторые базовые функции. Мы можем расширить метод, унаследовав этот класс. Подробности смотрите в кодеCustomUser.java
  • UserDetailsService.class: интерфейс, официально предоставленный Spring Security, содержащий только один метод.loadUserByUsername(), Spring Security вызовет этот метод для получения данных, существующих в базе данных, а затем сравнит их с именем пользователя и паролем, отправленными пользователем, чтобы определить, верны ли имя пользователя и пароль пользователя. Поэтому нам нужно реализовать это самимloadUserByUsername()Сюда. Подробности смотрите в кодеCustomUserDetailsService.java.

логика проекта

Чтобы отразить разницу в правах, мы построили базу данных через HashMap, которая содержит 4 пользователя.

ID имя пользователя пароль разрешение
1 jack jack123 user
2 danny danny123 editor
3 alice alice123 reviewer
4 smith smith123 admin

Опишите разрешения

user: самые основные разрешения, пока вы входите в систему для пользователейuserразрешение

editor:существуетuserбыли добавлены разрешенияeditorразрешение

reviewer: То же, что и выше,editorиreviewerотносятся к одному уровню власти

admin: содержит все разрешения

Для проверки разрешений предоставляем несколько страниц

URL-адрес инструкция Доступный
/ титульная страница доступный для всех (анонимно)
/login Страница авторизации доступный для всех (анонимно)
/logout страница выхода доступный для всех (анонимно)
/user/home Пользовательский центр user
/user/editor editor, admin
/user/reviewer reviewer, admin
/user/admin admin
/403 Страница ошибки 403, украшенная, вы можете использовать ее напрямую доступный для всех (анонимно)
/404 404 страница ошибок, украшена, вы можете использовать его напрямую доступный для всех (анонимно)
/500 Страница ошибки 500, украшенная, вы можете использовать ее напрямую доступный для всех (анонимно)

Конфигурация кода

Конфигурация Maven

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>org.inlighting</groupId>
    <artifactId>security-demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>security-demo</name>
    <description>Demo project for Spring Boot &amp; Spring Security</description>

    <!--指定JDK版本,大家可以改成自己的-->
    <properties>
        <java.version>11</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--对Thymeleaf添加Spring Security标签支持-->
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-test</artifactId>
            <scope>test</scope>
        </dependency>
        <!--开发的热加载配置-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

конфигурация application.properties

Для того, чтобы тепловая нагрузка (эта поправка шаблона без перезапуска Tomcat) вступила в силу, нам нужно профилировать в Spring Boot сверху с проходом

spring.thymeleaf.cache=false

Если вам нужно больше узнать о горячей загрузке, см. Официальную документацию:docs.spring.IO/весенняя загрузка…

Конфигурация безопасности Spring

Сначала мы включаем поддержку аннотаций методов: просто добавим класс@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)Аннотация, мы устанавливаемprePostEnabled = trueзаключается в поддержкеhasRole()такие выражения. Если вы хотите узнать больше об аннотации метода, вы можете увидетьIntroduction to Spring Method SecurityЭта статья.

SecurityConfig.java

/**
 * 开启方法注解支持,我们设置prePostEnabled = true是为了后面能够使用hasRole()这类表达式
 * 进一步了解可看教程:https://www.baeldung.com/spring-security-method-security
 */
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    /**
     * TokenBasedRememberMeServices的生成密钥,
     * 算法实现详见文档:https://docs.spring.io/spring-security/site/docs/5.1.3.RELEASE/reference/htmlsingle/#remember-me-hash-token
     */
    private final String SECRET_KEY = "123456";

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    /**
     * 必须有此方法,Spring Security官方规定必须要有一个密码加密方式。
     * 注意:例如这里用了BCryptPasswordEncoder()的加密方法,那么在保存用户密码的时候也必须使用这种方法,确保前后一致。
     * 详情参见项目中Database.java中保存用户的逻辑
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置Spring Security,下面说明几点注意事项。
     * 1. Spring Security 默认是开启了CSRF的,此时我们提交的POST表单必须有隐藏的字段来传递CSRF,
     * 而且在logout中,我们必须通过POST到 /logout 的方法来退出用户,详见我们的login.html和logout.html.
     * 2. 开启了rememberMe()功能后,我们必须提供rememberMeServices,例如下面的getRememberMeServices()方法,
     * 而且我们只能在TokenBasedRememberMeServices中设置cookie名称、过期时间等相关配置,如果在别的地方同时配置,会报错。
     * 错误示例:xxxx.and().rememberMe().rememberMeServices(getRememberMeServices()).rememberMeCookieName("cookie-name")
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login") // 自定义用户登入页面
                .failureUrl("/login?error") // 自定义登入失败页面,前端可以通过url中是否有error来提供友好的用户登入提示
                .and()
                .logout()
                .logoutUrl("/logout")// 自定义用户登出页面
                .logoutSuccessUrl("/")
                .and()
                .rememberMe() // 开启记住密码功能
                .rememberMeServices(getRememberMeServices()) // 必须提供
                .key(SECRET_KEY) // 此SECRET需要和生成TokenBasedRememberMeServices的密钥相同
                .and()
                /*
                 * 默认允许所有路径所有人都可以访问,确保静态资源的正常访问。
                 * 后面再通过方法注解的方式来控制权限。
                 */
                .authorizeRequests().anyRequest().permitAll()
                .and()
                .exceptionHandling().accessDeniedPage("/403"); // 权限不足自动跳转403
    }

    /**
     * 如果要设置cookie过期时间或其他相关配置,请在下方自行配置
     */
    private TokenBasedRememberMeServices getRememberMeServices() {
        TokenBasedRememberMeServices services = new TokenBasedRememberMeServices(SECRET_KEY, customUserDetailsService);
        services.setCookieName("remember-cookie");
        services.setTokenValiditySeconds(100); // 默认14天
        return services;
    }
}

UserService.java

Собственные операции по моделированию базы данныхService, используется для прохожденияHashMapСмоделированный источник данных получает данные.

@Service
public class UserService {

    private Database database = new Database();

    public CustomUser getUserByUsername(String username) {
        CustomUser originUser = database.getDatabase().get(username);
        if (originUser == null) {
            return null;
        }

        /*
         * 此处有坑,之所以这么做是因为Spring Security获得到User后,会把User中的password字段置空,以确保安全。
         * 因为Java类是引用传递,为防止Spring Security修改了我们的源头数据,所以我们复制一个对象提供给Spring Security。
         * 如果通过真实数据库的方式获取,则没有这种问题需要担心。
          */
        return new CustomUser(originUser.getId(), originUser.getUsername(), originUser.getPassword(), originUser.getAuthorities());
    }
}

CustomUserDetailsService.java

/**
 * 实现官方提供的UserDetailsService接口即可
 */
@Service
public class CustomUserDetailsService implements UserDetailsService {

    private Logger LOGGER = LoggerFactory.getLogger(getClass());

    @Autowired
    private UserService userService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        CustomUser user = userService.getUserByUsername(username);
        if (user == null) {
            throw new  UsernameNotFoundException("该用户不存在");
        }
        LOGGER.info("用户名:"+username+" 角色:"+user.getAuthorities().toString());
        return user;
    }
}

Пользовательская аннотация разрешения

В процессе разработки нашего веб-сайта, например,GET /user/editorРоль запросаEDITORиADMINКонечно, если мы напишем длинный список выражений разрешений для каждого метода, который должен оценивать разрешения, это должно быть очень сложно. Но с пользовательскими аннотациями разрешений мы можем передать@IsEditorТак намного проще судить. Для получения дополнительной информации см.:Introduction to Spring Method Security

IsUser.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyAuthority('ROLE_USER', 'ROLE_EDITOR', 'ROLE_REVIEWER', 'ROLE_ADMIN')")
public @interface IsUser {
}

IsEditor.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_EDITOR', 'ROLE_ADMIN')")
public @interface IsEditor {
}

IsReviewer.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_USER', 'ROLE_REVIEWER', 'ROLE_ADMIN')")
public @interface IsReviewer {
}

IsAdmin.java

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("hasAnyRole('ROLE_ADMIN')")
public @interface IsAdmin { 
}

Spring Security поставляется с выражениями

  • hasRole(), иметь ли определенное разрешение

  • hasAnyRole(), достаточно одного из нескольких разрешений, напримерhasAnyRole("ADMIN","USER")

  • hasAuthority(),AuthorityиRoleочень похоже, единственное отличиеAuthorityбольше префиксовROLE_,какhasAuthority("ROLE_ADMIN")ЭквивалентноhasRole("ADMIN"), вы можете обратиться к вышеIsUser.javaнаписание

  • hasAnyAuthority(), как и выше, только одно из нескольких разрешений может

  • permitAll(), denyAll(),isAnonymous(), isRememberMe(), можно понимать буквально

  • isAuthenticated(), isFullyAuthenticated(), разница между нимиisFullyAuthenticated()Требования безопасности для аутентификации выше. Например, пользователь проходитФункция «Запомнить пароль»Войдите в систему для выполнения конфиденциальных операций,isFullyAuthenticated()вернусьfalse, после чего мы можем попросить пользователя снова ввести пароль для безопасности, иisAuthenticated()Возврат до тех пор, пока вошедший в систему пользовательtrue.

  • principal(), authentication(), например, если мы хотим получить идентификатор вошедшего в систему пользователя, мы можем передатьprincipal()возвращениеObjectПолучите, на самом делеprincipal()возвращениеObjectВ основном эквивалентно тому, что мы написали самиCustomUser. иauthentication()возвращениеAuthenticationдаPrincipalРодительский класс связанных операций можно найти вAuthenticationисходный код. Для получения дополнительной информации см. нижеЧетыре метода получения пользовательских данных в записи контроллера

  • hasPermission(), вы можете обратиться к буквальному значению

Intro to Spring Security Expressions.

Добавить опору Thymeleaf

мы проходимthymeleaf-extras-springsecurityчтобы добавить поддержку Thymeleaf для Spring Security.

Конфигурация Maven

Вышеупомянутая конфигурация Maven была добавлена

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

Пример использования

Обратите внимание, что мы собираемся добавить в htmlxmlns:secслужба поддержки

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<head>
    <meta charset="UTF-8">
    <title>Admin</title>
</head>
<body>
<p>This is a home page.</p>
<p>Id: <th:block sec:authentication="principal.id"></th:block></p>
<p>Username: <th:block sec:authentication="principal.username"></th:block></p>
<p>Role: <th:block sec:authentication="principal.authorities"></th:block></p>
</body>
</html>

Если вы хотите узнать больше, обратитесь к документации.thymeleaf-extras-springsecurity.

Написание контроллера

IndexController.java

У этого контроллера нет полномочий

@Controller
public class IndexController {

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

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

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

UserController.java

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

@IsUser // 表明该控制器下所有请求都需要登入后才能访问
@Controller
@RequestMapping("/user")
public class UserController {

    @GetMapping("/home")
    public String home(Model model) {
        // 方法一:通过SecurityContextHolder获取
        CustomUser user = (CustomUser)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        model.addAttribute("user", user);
        return "user/home";
    }

    @GetMapping("/editor")
    @IsEditor
    public String editor(Authentication authentication, Model model) {
        // 方法二:通过方法注入的形式获取Authentication
        CustomUser user = (CustomUser)authentication.getPrincipal();
        model.addAttribute("user", user);
        return "user/editor";
    }

    @GetMapping("/reviewer")
    @IsReviewer
    public String reviewer(Principal principal, Model model) {
        // 方法三:同样通过方法注入的方法,注意要转型,此方法很二,不推荐
        CustomUser user = (CustomUser) ((Authentication)principal).getPrincipal();
        model.addAttribute("user", user);
        return "user/reviewer";
    }

    @GetMapping("/admin")
    @IsAdmin
    public String admin() {
        // 方法四:通过Thymeleaf的Security标签进行,详情见admin.html
        return "user/admin";
    }
}

Уведомление

  • Если метод A с контролем безопасности вызывается другим методом в том же классе, управление разрешениями метода A будет игнорироваться, а также будут затронуты частные методы.
  • веснаSecurityContextОн привязан к потоку.Если мы создадим другие потоки в текущем потоке, то ихSecurityContextОн не является общим, для получения дополнительной информации см.Spring Security Context Propagation with @Async

Написание HTML

При написании HTML, в основном похоже, то есть обратите внимание, что **, если вы откроете CSRF, при приготовлении формы Post запрос на добавление скрытого поля, такого как **<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}"/> , но на самом деле вам не нужно его добавлять, потому что Thymeleaf добавит его автоматически😀.

Суммировать

Учебник грубый, приветствуем исправления!

Для более глубокого понимания, если вы хотите учиться систематически, вы можете посмотретьSecurity with Spring.