Spring Boot 3.0 Security 6 のカスタマイズされた UserDetailsService、動的アクセス許可、Thymeleaf、パスワード強度、有効期限、ロック、ロック解除、無効化、過去の新しいパスワード編集距離、ログイン ログ、Envers

このチュートリアルでは、フォームベースの認証を備えた Spring Security API を使用して、Spring Boot アプリケーションの Web ページを保護するコードを記述する方法を説明します。ユーザーの詳細は MySQL データベースに保存され、Spring JDBC を使用してデータベースに接続されます。このチュートリアルでは ProductManager プロジェクトから始めて、ログインおよびログアウト機能を既存の Spring Boot プロジェクトに追加します。

1. ユーザーテーブルと仮想認証情報を作成する

資格情報はデータベースに保存する必要があります。テーブルの自動作成には Spring Data JPA を使用します。テーブル間の関係の ER 図は次のとおりです。

2. データソースのプロパティを構成する

次に、次のようにアプリケーション プロパティ ファイルでデータベース接続情報を指定します。 MySQL データベースに従って URL、ユーザー名、およびパスワードを更新します。

spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://localhost:3306/product3?autoReconnect=true&useSSL=false&useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai
spring.datasource.username=root
spring.datasource.password=root
#logging.level.root=WARN

3. Spring Security と MySQL JDBC Driver への依存関係を宣言する

プロジェクトで Spring Security API を使用するには、pom.xml ファイルで次の依存関係を宣言します。 Spring Boot および MySQL で JDBC を使用するには: 依存関係のバージョンは Spring Boot Starter 親プロジェクトによってすでに定義されていることに注意してください。

<?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>3.0.0</version>
        <relativePath /> <!-- lookup parent from repository -->
    </parent>
    <groupId>net.codejava</groupId>
    <artifactId>ProductManagerUserDetailsServiceAuditBoot3.0</artifactId>
    <version>2.0</version>
    <name>ProductManagerUserDetailsServiceAuditBoot3.0</name>
    <description>Spring Boot CRUD Web App Example</description>
    <packaging>jar</packaging>

    <properties>
        <java.version>1.8</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</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>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.thymeleaf.extras</groupId>
            <artifactId>thymeleaf-extras-springsecurity6</artifactId>
        </dependency>
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.24</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.passay</groupId>
            <artifactId>passay</artifactId>
            <version>1.6.2</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-text</artifactId>
            <version>1.10.0</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-envers</artifactId>
            <version>6.1.5.Final</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId>
            <version>2.0.1.Final</version>
        </dependency>


    </dependencies>

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

</project>

4. Spring Securityの構成

フォームベースの認証および CustomUserDetailsS​​ervice で Spring セキュリティを使用するには、次のように WebSecurityConfig クラスを作成します。

package com.example;

import java.util.Arrays;
import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.session.HttpSessionEventPublisher;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig {

    @Autowired
    private DataSource dataSource;

    @Autowired
    private CustomLoginFailureHandler loginFailureHandler;
    @Autowired
    private CustomLoginSuccessHandler loginSuccessHandler;

    @Autowired

    private CustomUserDetailsService customUserDetailsService;

//    @Autowired
//    public void configAuthentication(AuthenticationManagerBuilder authBuilder) throws Exception {
//        authBuilder.userDetailsService(customUserDetailsService)
//                .passwordEncoder(new BCryptPasswordEncoder());
//
//    }
//requestMatchers("/**").
    @Bean
    VerifyCodeAuthenticationProvider authenticationProvider() {
        VerifyCodeAuthenticationProvider authenticationProvider = new VerifyCodeAuthenticationProvider();
        authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
        authenticationProvider.setUserDetailsService(customUserDetailsService);
        return authenticationProvider;
    }

    @Bean
    protected AuthenticationManager authenticationManager() throws Exception {
        return new ProviderManager(Arrays.asList(authenticationProvider()));
    }

    @Autowired
    CustomAuthorizationManager customAuthorizationManager;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        http
                .authorizeHttpRequests()
                .requestMatchers("/403").permitAll()
                .requestMatchers("/captcha_image").permitAll()
                .requestMatchers("/header").permitAll()
                .requestMatchers("/footer").permitAll()
                .requestMatchers("/sidebar").permitAll()
                .requestMatchers("/index2").permitAll()
                .requestMatchers("/index3").permitAll()
                .requestMatchers("/pages/**").permitAll()
                .requestMatchers("/css/**").permitAll()
                .requestMatchers("/js/**").permitAll()
                .requestMatchers("/assets/**").permitAll()
                .requestMatchers("/webjars/**").permitAll()
                .requestMatchers("/common/**").permitAll()
                .requestMatchers("/login").permitAll()
                .requestMatchers("/logout").permitAll()
                .requestMatchers("/verify").permitAll()
                .requestMatchers("/home").authenticated()
                .requestMatchers("/user/info").authenticated()
                .requestMatchers("/change/password").authenticated()
                .requestMatchers("/new/password").authenticated()
                .requestMatchers("/").authenticated()
                .anyRequest()
                //                .authenticated()
                .access(customAuthorizationManager)
                .and()
                .formLogin().loginPage("/login")
                .permitAll()
                .failureHandler(loginFailureHandler)
                .successHandler(loginSuccessHandler)
                .and()
                .logout().permitAll()
                .and()
                .exceptionHandling().accessDeniedPage("/403");
        http
                .sessionManagement()
                .maximumSessions(1)
                .expiredUrl("/")
                .maxSessionsPreventsLogin(false)
                .sessionRegistry(sessionRegistry());
        return http.build();
    }

    @Bean
    public SessionRegistry sessionRegistry() {
        SessionRegistry sessionRegistry = new SessionRegistryImpl();
        return sessionRegistry;
    }

    // Register HttpSessionEventPublisher
    @Bean
    public static ServletListenerRegistrationBean httpSessionEventPublisher() {
        return new ServletListenerRegistrationBean(new HttpSessionEventPublisher());
    }

    @Bean
    BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

このセキュリティ構成クラスは、@EnableWebSecurity アノテーションを付ける必要があり、Web Security Configurator アダプターのサブクラスです。

CustomUserDetailsS​​ervice

package com.example;

import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    public UserDetails loadUserByUsername(String userName)
            throws UsernameNotFoundException {
        User domainUser = userRepository.findByUsername(userName);

        if (domainUser != null) {
            return new org.springframework.security.core.userdetails.User(
                    domainUser.getUsername(),
                    domainUser.getPassword(),
                    domainUser.getEnabled(),
                    domainUser.getAccountNonExpired(),
                    domainUser.getCredentialsNonExpired(),
                    domainUser.getAccountNonLocked(),
                    getAuthorities(domainUser.getRoles())
            );
        }
//        return null;
        throw new UsernameNotFoundException("User " + userName + " does not exist");
    }

    private Collection<? extends GrantedAuthority> getAuthorities(
            Collection<Role> roles) {
        List<GrantedAuthority> authorities
                = new ArrayList<>();
        for (Role role : roles) {
//            authorities.add(new SimpleGrantedAuthority(role.getName()));
            role.getPermissions().stream()
                    .map(p -> new SimpleGrantedAuthority(p.getName()))
                    .forEach(authorities::add);
        }

        return authorities;
    }

}

ユーザー

package com.example;

import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
import lombok.Data;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.envers.Audited;

@Data
@Entity
@DynamicUpdate
@Audited
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String username;

    private String password;

    private Boolean enabled = true;

    private Boolean accountNonExpired = true;

    private Boolean credentialsNonExpired = true;

    private Boolean accountNonLocked = true;

    private String email;

    private String name;

    private String homepage;
    @ManyToMany(fetch = FetchType.EAGER)
    @JoinTable(
            name = "user_role",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id")
    )
    private Set<Role> roles = new HashSet<>();

    @Column(name = "password_changed_time")
    private Date passwordChangedTime;

    @Column(name = "failed_attempt")
    private int failedAttempt;

    @Column(name = "lock_time")
    private Date lockTime;

    @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.MERGE})
    @JoinTable(
            name = "user_history",
            joinColumns = @JoinColumn(name = "user_id"),
            inverseJoinColumns = @JoinColumn(name = "history_id")
    )
    private Set<History> historys = new HashSet<>();

}

役割と権限は連携して機能します

5. ログイン認証プロセス

package com.example;

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

@Controller
public class LoginController {

    @GetMapping("/login")
    public String viewLoginPage() {
        // custom logic before showing login page...

        return "login";
    }

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

    @GetMapping("/403")
    public String view403Page() {
        return "403";
    }
}
package com.example;

import com.google.code.kaptcha.Constants;

import java.util.Objects;
import jakarta.servlet.http.HttpServletRequest;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;

import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

public class VerifyCodeAuthenticationProvider extends DaoAuthenticationProvider {

    @Override
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        //获取当前请求
        HttpServletRequest req = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        String code = req.getParameter("kaptcha");//从当前请求中拿到code参数
        System.out.println("!!!code=" + code);
        String verifyCode = (String) req.getSession().getAttribute(Constants.KAPTCHA_SESSION_KEY);//从session中获取生成的验证码字符串
        System.out.println("!!!" + Constants.KAPTCHA_SESSION_KEY + "=" + verifyCode);
        //比较验证码是否相同
        if (StringUtils.isBlank(code) || StringUtils.isBlank(verifyCode) || !Objects.equals(code, verifyCode)) {
            throw new AuthenticationServiceException("验证码错误!");
        }
        super.additionalAuthenticationChecks(userDetails, authentication);//调用父类DaoAuthenticationProvider的方法做密码的校验
    }
}

キャプチャ確認コード

package com.example;

import java.awt.image.BufferedImage;

import javax.imageio.ImageIO;
import jakarta.servlet.ServletOutputStream;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import com.google.code.kaptcha.Constants;
import com.google.code.kaptcha.Producer;

/**
 * @author will
 */
@Controller
public class KaptchaController {

    @Autowired
    private Producer captchaProducer;

    @GetMapping("/captcha_image")
    public void getKaptchaImage(HttpServletResponse response, HttpSession session) throws Exception {
        response.setDateHeader("Expires", 0);
        // Set standard HTTP/1.1 no-cache headers.
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        // Set IE extended HTTP/1.1 no-cache headers (use addHeader).
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        // Set standard HTTP/1.0 no-cache header.
        response.setHeader("Pragma", "no-cache");
        // return a jpeg
        response.setContentType("image/jpeg");
        // create the text for the image
        String capText = captchaProducer.createText();
        // store the text in the session
        // request.getSession().setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);

        // save captcha to session
        session.setAttribute(Constants.KAPTCHA_SESSION_KEY, capText);

        // create the image with the text
        BufferedImage bi = captchaProducer.createImage(capText);
        ServletOutputStream out = response.getOutputStream();

        // write the data out
        ImageIO.write(bi, "jpg", out);
        try {
            out.flush();
        } finally {
            out.close();
        }
    }
}
package com.example;

import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.stereotype.Component;

import java.util.Properties;

@Component
public class KaptchaConfig {

    @Bean
    public DefaultKaptcha getDefaultKaptcha() {
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        Properties properties = new Properties();
        properties.put("kaptcha.border", "no");
        properties.put("kaptcha.textproducer.font.color", "black");
        properties.put("kaptcha.image.width", "150");
        properties.put("kaptcha.image.height", "40");
        properties.put("kaptcha.textproducer.font.size", "30");
        properties.put("kaptcha.session.key", "verifyCode");
        properties.put("kaptcha.textproducer.char.space", "5");
//        properties.setProperty("kaptcha.textproducer.char.length", "4");
        Config config = new Config(properties);
        defaultKaptcha.setConfig(config);

        return defaultKaptcha;
    }

}

6. ログインページ

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <meta http-equiv="X-UA-Compatible" content="IE=edge">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>Bootstrap 5 Sign In Form with Image Example</title>
        <link href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css" rel="stylesheet"
              integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
        <script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/js/bootstrap.bundle.min.js"
                integrity="sha384-MrcW6ZMFYlzcLA8Nl+NtUVF0sA7MsXsP1UyJoMp4YLEuNSfAP+JcXn/tWtIaxVXM"
        crossorigin="anonymous"></script>
        <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/font/bootstrap-icons.css">
    </head>

    <body>

        <form th:action="@{/login}" method="post">
            <div class="container-fluid vh-100" style="margin-top:50px">
                <div class="" style="margin-top:50px">
                    <div class="rounded d-flex justify-content-center">
                        <div class=" col-md-4 col-sm-12 shadow-lg p-5 bg-light">
                            <div th:if="${param.error}">
                                <p class="text-danger">[[${session.SPRING_SECURITY_LAST_EXCEPTION.message}]]</p>
                            </div>

                            <div th:if="${param.logout}">
                                <p class="text-warning">You have been logged out.</p>
                            </div>
                            <div class="text-center">
                                <h3 class="text-primary">请登录</h3>
                            </div>
                            <div class="p-4">
                                <div class="input-group mb-3">
                                    <span class="input-group-text bg-secondary"><i
                                            class="bi bi-person-fill text-white"></i></span>
                                    <input id="username" type="text" name="username" required class="form-control" placeholder="用户名">
                                </div>
                                <div class="input-group mb-3">
                                    <span class="input-group-text bg-secondary"><i
                                            class="bi bi-key-fill text-white"></i></span>
                                    <input  id="password" type="password" name="password" required class="form-control" placeholder="密码">
                                </div>
                                <div class="input-group mb-3">
                                    <span class="input-group-text bg-secondary"><i
                                            class="bi bi-lock-fill text-white"></i></span>
                                    <input type="text" name="kaptcha"  class="form-control" placeholder="输入下图中的校验码">
                                </div>
                                <div class="input-group mb-3">
                                    <span class="input-group-text bg-secondary"><i
                                            class="bi bi-image-fill text-white"></i></span>
                                    <img alt="Click the picture to refresh!" class="pointer" th:src="@{/captcha_image}"
                                         onclick="this.src = '/captcha_image?d=' + new Date() * 1">
                                </div>
                                <div class="col-12">
                                    <button type="submit" class="btn btn-primary px-4 float-end mt-4">登录</button>
                                </div>

                            </div>
                        </div>
                    </div>
                </div>
            </div>
    </body>

</html>

5. ログインとログアウトのテスト

Spring Boot アプリケーションを起動し、Web ブラウザで http://localhost:8080 にアクセスすると、カスタマイズされたログイン ページが表示されます。

 ここで、正しいユーザー名 admin とパスワード admin を入力すると、次のようなホームページが表示されます。

 そして、ユーザー名が続くウェルカム メッセージに注目してください。これで、ユーザーはアプリケーションを使用することが認証されました。「ログアウト」ボタンをクリックすると、カスタマイズされたログイン ページが表示されます。これは、Spring Boot アプリケーションへのログインとログアウトが正常に実装されたことを意味します。

データベースからの動的権限検証をカスタマイズする

package com.example;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
import org.hibernate.annotations.DynamicUpdate;
import org.hibernate.envers.Audited;

@Data
@Entity
@DynamicUpdate
@Audited
public class Permission {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private String description;
    private String uri;
    private String method;

    @Override
    public String toString() {
        return this.name;
    }
}
package com.example;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PermissionRepository extends JpaRepository<Permission, Long> {

    public Permission findByName(String name);

}
package com.example;

import jakarta.servlet.http.HttpServletRequest;
import java.util.Collection;
import java.util.function.Supplier;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;

@Component
public class CustomAuthorizationManager implements AuthorizationManager<RequestAuthorizationContext> {

    @Autowired
    private PermissionRepository permissionRepository;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public AuthorizationDecision check(Supplier<Authentication> authentication, RequestAuthorizationContext requestAuthorizationContext) {
        HttpServletRequest request = requestAuthorizationContext.getRequest();
        //获取用户认证信息
        System.out.println(authentication.get().getAuthorities());
        Object principal = authentication.get().getPrincipal();
        System.out.println(principal.getClass());
        //判断数据是否为空 以及类型是否正确
        if (null != principal && principal instanceof org.springframework.security.core.userdetails.User) {
            String username = ((org.springframework.security.core.userdetails.User) principal).getUsername();
            System.out.println(username);

        }

        String requestURI = request.getRequestURI();
        System.out.println(requestURI);
        String method = request.getMethod();
        System.out.println(method);

        Collection<? extends GrantedAuthority> authorities = authentication.get().getAuthorities();
        boolean hasPermission = false;
        for (GrantedAuthority authority : authorities) {
            String authorityname = authority.getAuthority();
            System.out.println(authority.getAuthority());
            Permission permission = permissionRepository.findByName(authorityname);
            System.out.println(permissionRepository.findByName(authorityname));
            if (null != permission && permission.getMethod().equals(request.getMethod()) && antPathMatcher.match(permission.getUri(), request.getRequestURI())) {
                hasPermission = true;

                break;
            }
        }
        System.out.println(hasPermission);
        return new AuthorizationDecision(hasPermission);
    }

}
package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Date;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;

@Component
public class CustomLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    @Autowired
    private UserLoginService userLoginService;
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private LoginLogRepository loginLogRepository;

    private static final long PASSWORD_EXPIRATION_TIME = 30L * 24L * 60L * 60L * 1000L;    // 30 days

    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

        UserDetails userDetails = (UserDetails) authentication.getPrincipal();
        String username = userDetails.getUsername();
        User user = userRepository.getByUsername(username);
        if (user.getFailedAttempt() > 0) {
            userLoginService.resetFailedAttempts(user.getUsername());
        }
        System.out.println(request.getRemoteAddr());
        System.out.println(request.getSession().getId());
        LoginLog loginLog = new LoginLog();
        loginLog.setUsername(username);
        loginLog.setDescription("登录成功");
        loginLog.setIp(request.getRemoteAddr());
        loginLog.setEventtime(new Date());
        loginLog.setSessionid(request.getSession().getId());
        loginLogRepository.save(loginLog);

        if (user.getPasswordChangedTime() == null) {
            String redirectURL = request.getContextPath() + "/change/password";
            response.sendRedirect(redirectURL);

        } else {
            long currentTime = System.currentTimeMillis();
            long lastChangedTime = user.getPasswordChangedTime().getTime();

            if (currentTime > lastChangedTime + PASSWORD_EXPIRATION_TIME) {
                System.out.println("User:" + user.getUsername() + ":password expired");
                System.out.println("Last Time password changed:" + user.getPasswordChangedTime());
                System.out.println("Current Time:" + new Date());

                String redirectURL = request.getContextPath() + "/change/password";
                response.sendRedirect(redirectURL);
            } else {

                System.out.println(request.getContextPath() + user.getHomepage());
                response.sendRedirect(request.getContextPath() + user.getHomepage());
//        super.onAuthenticationSuccess(request, response, authentication);
            }
        }
    }
}
package com.example;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Date;
import org.springframework.security.authentication.CredentialsExpiredException;

@Component
public class CustomLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private UserRepository userRepository;
    @Autowired
    private UserLoginService userLoginService;
    @Autowired
    private LoginLogRepository loginLogRepository;

    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception)
            throws IOException, ServletException {
        if (exception instanceof CredentialsExpiredException) {
            // 保存异常信息到会话属性,供页面显示
            saveException(request, exception);
            String userName = request.getParameter("username");
            // 跳转到修改密码页面
            response.sendRedirect("/changepassword?error&username=" + userName);
        }

        String username = request.getParameter("username");
        User user = userRepository.getByUsername(username);
        if (user != null) {
            LoginLog loginLog = new LoginLog();
            loginLog.setUsername(username);
            loginLog.setDescription("登录失败");
            loginLog.setIp(request.getRemoteAddr());
            loginLog.setEventtime(new Date());
            loginLog.setSessionid(request.getSession().getId());
            loginLogRepository.save(loginLog);
            if (user.getEnabled() && user.getAccountNonLocked()) {
                if (user.getFailedAttempt() < userLoginService.MAX_FAILED_ATTEMPTS - 1) {
                    System.out.println("user.getFailedAttempt()=" + user.getFailedAttempt());
                    userLoginService.increaseFailedAttempts(user);
                } else {

                    userLoginService.lock(user);
                    exception = new LockedException("your account has been locked due to 3 failed attempt"
                            + " It will be unclocked after 15 minutes");
                    System.out.println(exception);
                    System.out.println("userLoginService.lock(user)");
                }
            } else if (!user.getAccountNonLocked()) {
                if (userLoginService.unlockWhenTimeExpired(user)) {
                    exception = new LockedException("your account has been unclock ."
                            + " please try to login again");
                    System.out.println(exception);
                }
            }
        }

        super.setDefaultFailureUrl("/login?error");
        super.onAuthenticationFailure(request, response, exception);
    }
}

thymeleaf ビュー ファイルでは、アクセス許可に基づいて接続メニューが表示されます

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta content="width=device-width, initial-scale=1.0" name="viewport">
        <title>Dashboard - Admin Bootstrap Template</title>
        <meta name="robots" content="noindex, nofollow">
        <meta content="" name="description">
        <meta content="" name="keywords">
        <link href="/assets/img/favicon.png" rel="icon">
        <link href="/assets/img/apple-touch-icon.png" rel="apple-touch-icon">
        <link href="/assets/css/bootstrap.min.css" rel="stylesheet">
        <link href="/assets/css/bootstrap-icons.css" rel="stylesheet">
        <link href="/assets/css/boxicons.min.css" rel="stylesheet">
        <link href="/assets/css/quill.snow.css" rel="stylesheet">
        <link href="/assets/css/quill.bubble.css" rel="stylesheet">
        <link href="/assets/css/remixicon.css" rel="stylesheet">
        <link href="/assets/css/simple-datatables.css" rel="stylesheet">
        <link href="/assets/css/style.css" rel="stylesheet">
    </head>
    <body>
        <header id="header" class="header fixed-top d-flex align-items-center" th:replace="fragments/header :: header">
        </header>
        <aside id="sidebar" class="sidebar" th:replace="fragments/sidebar :: sidebar">
        </aside>
        <main id="main" class="main">
            <div class="pagetitle">
                <nav>
                    <ol class="breadcrumb">
                        <li class="breadcrumb-item"><a href="index.html">主页</a></li>
                        <li class="breadcrumb-item active">用户管理</li>
                    </ol>
                </nav>
            </div>
            <section class="section dashboard">
                <div class="row">
                    <div class="col-lg-12">
                        <div class="card">
                            <div class="card-body">

                                <br/>
                                <br/>
                                <div class="table-title">
                                    <div class="row">
                                        <div class="col-sm-6">
                                            <h2> <b>用户管理</b></h2>
                                        </div>
                                        <div class="col-sm-6">
                                            <div sec:authorize="hasAnyAuthority('permission_create')">
                                                <a href="/user/new" class="btn btn-success" ><i class="bi bi-person-plus" data-toggle="tooltip" title="添加用户"></i></a>
                                            </div>
                                        </div>
                                    </div>
                                </div>
                                <table class="table table-striped table-hover">
                                    <thead >
                                        <tr>
                                            <th>ID</th>
                                            <th>User Name</th>
                                            <th>E-mail</th>
                                            <th>Name</th>
                                            <th>Home Page</th>
                                            <th>Roles</th>
                                            <th>Actions</th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        <tr th:each="user: ${listUsers}">
                                            <td th:text="${user.id}">User ID</td>
                                            <td th:text="${user.username}">User Name</td>
                                            <td th:text="${user.email}">E-mail</td>
                                            <td th:text="${user.name}">Name</td>
                                            <td th:text="${user.homepage}">Home Page</td>
                                            <td th:text="${user.roles}">Roles</td>
                                            <td>
                                                <a sec:authorize="hasAuthority('user_edit')" th:href="@{'/user/edit/' + ${user.id}}" class="edit" ><i class="bi bi-pencil" data-toggle="tooltip" title="编辑"></i></a>
                                                <a sec:authorize="hasAuthority('user_edit')" th:href="@{'/user/resetpassword/' + ${user.id}}" class="reset" ><i class="bi bi-key" data-toggle="tooltip" title="重置密码"></i></a>
                                                <a sec:authorize="hasAuthority('user_delete')" th:href="@{'/user/delete/' + ${user.id}}" class="delete" ><i class="bi bi-trash" data-toggle="tooltip" title="删除"></i></a>


                                            </td>
                                        </tr>
                                    </tbody>
                                </table>



                            </div>
                        </div>
                    </div>
            </section>
        </main>
        <footer id="footer" class="footer" th:replace="fragments/footer :: footer">
            <div class="copyright"> &copy; Copyright <strong><span>Compnay Name</span></strong>. All Rights Reserved</div>
            <div class="credits"> with love <a href="https://freeetemplates.com/">FreeeTemplates</a></div>
        </footer>
        <a href="#" class="back-to-top d-flex align-items-center justify-content-center"><i class="bi bi-arrow-up-short"></i></a>
        <script src="/assets/js/apexcharts.min.js"></script>
        <script src="/assets/js/bootstrap.bundle.min.js"></script>
        <script src="/assets/js/chart.min.js"></script>
        <script src="/assets/js/echarts.min.js"></script>
        <script src="/assets/js/quill.min.js"></script>
        <script src="/assets/js/simple-datatables.js"></script>
        <script src="/assets/js/tinymce.min.js"></script>
        <script src="/assets/js/validate.js"></script>
        <script src="/assets/js/main.js"></script>

    </body>
</html>

 Thymeleaf集成Bootstrap 5 無料管理ダッシュボード テンプレート 1 - freeetemplates

ヘッダー.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"  xmlns:sec="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
            <meta content="width=device-width, initial-scale=1.0" name="viewport">
                <title>Dashboard - Admin Bootstrap Template</title>
                <meta name="robots" content="noindex, nofollow">
                    <meta content="" name="description">
                        <meta content="" name="keywords">
                            <link href="assets/img/favicon.png" rel="icon">
                                <link href="assets/img/apple-touch-icon.png" rel="apple-touch-icon">
                                    <link href="https://fonts.gstatic.com" rel="preconnect">
                                        <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
                                            <link href="assets/css/bootstrap.min.css" rel="stylesheet">
                                                <link href="assets/css/bootstrap-icons.css" rel="stylesheet">
                                                    <link href="assets/css/boxicons.min.css" rel="stylesheet">
                                                        <link href="assets/css/quill.snow.css" rel="stylesheet">
                                                            <link href="assets/css/quill.bubble.css" rel="stylesheet">
                                                                <link href="assets/css/remixicon.css" rel="stylesheet">
                                                                    <link href="assets/css/simple-datatables.css" rel="stylesheet">
                                                                        <link href="assets/css/style.css" rel="stylesheet">
                                                                            </head>
                                                                            <body>

                                                                                <header id="header" class="header fixed-top d-flex align-items-center" th:fragment="header">
                                                                                    <form th:action="@{/logout}" method="post" th:hidden="true" name="logoutForm">
                                                                                        <input type="submit" value="Logout" />
                                                                                    </form>
                                                                                    <div class="d-flex align-items-center justify-content-between"> <a href="index.html" class="logo d-flex align-items-center"> <img src="/assets/img/logo.png" alt=""> <span class="d-none d-lg-block">CRUD和RBAC系统</span> </a> <i class="bi bi-list toggle-sidebar-btn"></i></div>

                                                                                    <nav class="header-nav ms-auto">
                                                                                        <ul class="d-flex align-items-center">
                                                                                            <li class="nav-item d-block d-lg-none"> <a class="nav-link nav-icon search-bar-toggle " href="#"> <i class="bi bi-search"></i> </a></li>


                                                                                            <li class="nav-item dropdown pe-3">
                                                                                                <a class="nav-link nav-profile d-flex align-items-center pe-0" href="#" data-bs-toggle="dropdown"> <img src="/assets/img/profile.png" alt="Profile" class="rounded-circle"> <span class="d-none d-md-block dropdown-toggle ps-2"><span sec:authentication="name">Username</span></span> </a>
                                                                                                <ul class="dropdown-menu dropdown-menu-end dropdown-menu-arrow profile">
                                                                                                    <li class="dropdown-header">
                                                                                                        <h6><span sec:authentication="name">Username</span></h6>
                                                                                                    </li>
                                                                                                    <li>
                                                                                                        <hr class="dropdown-divider">
                                                                                                    </li>
                                                                                                    <li> <a class="dropdown-item d-flex align-items-center" href="/user/info"> <i class="bi bi-person"></i> <span>账号信息</span> </a></li>
                                                                                                    <li>
                                                                                                        <hr class="dropdown-divider">
                                                                                                    </li>
                                                                                                    <li> <a href="javascript: document.logoutForm.submit()" class="dropdown-item d-flex align-items-center" > <i class="bi bi-box-arrow-right"></i> <span>注销</span> </a>
                                                                                                    </li>

                                                                                                </ul>
                                                                                            </li>
                                                                                        </ul>
                                                                                    </nav>
                                                                                </header>
                                                                            </body>
                                                                            </html>

フッター.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"  xmlns:sec="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
            <meta content="width=device-width, initial-scale=1.0" name="viewport">
                <title>Dashboard - Admin Bootstrap Template</title>
                <meta name="robots" content="noindex, nofollow">
                    <meta content="" name="description">
                        <meta content="" name="keywords">
                            <link href="assets/img/favicon.png" rel="icon">
                                <link href="assets/img/apple-touch-icon.png" rel="apple-touch-icon">
                                    <link href="https://fonts.gstatic.com" rel="preconnect">
                                        <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
                                            <link href="assets/css/bootstrap.min.css" rel="stylesheet">
                                                <link href="assets/css/bootstrap-icons.css" rel="stylesheet">
                                                    <link href="assets/css/boxicons.min.css" rel="stylesheet">
                                                        <link href="assets/css/quill.snow.css" rel="stylesheet">
                                                            <link href="assets/css/quill.bubble.css" rel="stylesheet">
                                                                <link href="assets/css/remixicon.css" rel="stylesheet">
                                                                    <link href="assets/css/simple-datatables.css" rel="stylesheet">
                                                                        <link href="assets/css/style.css" rel="stylesheet">
                                                                            </head>
                                                                            <body>


                                                                                <footer id="footer" class="footer"  th:fragment="footer">
                                                                                    <div class="copyright"> &copy; Copyright <strong><span>CRUD和RBAC系统</span></strong>. All Rights Reserved</div>
                                                                                    <div class="credits"> with love <a href="https://freeetemplates.com/">FreeeTemplates</a></div>
                                                                                </footer>
                                                                            </body>
                                                                            </html>

サイドバー.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"  xmlns:sec="http://www.thymeleaf.org">
    <head>
        <meta charset="utf-8">
            <meta content="width=device-width, initial-scale=1.0" name="viewport">
                <title>Dashboard - Admin Bootstrap Template</title>
                <meta name="robots" content="noindex, nofollow">
                    <meta content="" name="description">
                        <meta content="" name="keywords">
                            <link href="assets/img/favicon.png" rel="icon">
                                <link href="assets/img/apple-touch-icon.png" rel="apple-touch-icon">
                                    <link href="https://fonts.gstatic.com" rel="preconnect">
                                        <link href="https://fonts.googleapis.com/css?family=Open+Sans:300,300i,400,400i,600,600i,700,700i|Nunito:300,300i,400,400i,600,600i,700,700i|Poppins:300,300i,400,400i,500,500i,600,600i,700,700i" rel="stylesheet">
                                            <link href="assets/css/bootstrap.min.css" rel="stylesheet">
                                                <link href="assets/css/bootstrap-icons.css" rel="stylesheet">
                                                    <link href="assets/css/boxicons.min.css" rel="stylesheet">
                                                        <link href="assets/css/quill.snow.css" rel="stylesheet">
                                                            <link href="assets/css/quill.bubble.css" rel="stylesheet">
                                                                <link href="assets/css/remixicon.css" rel="stylesheet">
                                                                    <link href="assets/css/simple-datatables.css" rel="stylesheet">
                                                                        <link href="assets/css/style.css" rel="stylesheet">
                                                                            </head>
                                                                            <body>



                                                                                <aside id="sidebar" class="sidebar" th:fragment="sidebar">
                                                                                    <ul class="sidebar-nav" id="sidebar-nav">
                                                                                        <li class="nav-item"> <a class="nav-link " href="index.html"> <i class="bi bi-grid"></i> <span>Dashboard</span> </a></li>
                                                                                        <li class="nav-item">
                                                                                            <a class="nav-link collapsed" data-bs-target="#components-nav" data-bs-toggle="collapse" href="#"> <i class="bi bi-menu-button-wide"></i><span>系统管理</span><i class="bi bi-chevron-down ms-auto"></i> </a>
                                                                                            <ul id="components-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
                                                                                                <li sec:authorize="hasAuthority('user')"><a th:href="@{/user}"><i class="bi bi-circle"></i><span>用户管理</span> </a></li>
                                                                                                <li sec:authorize="hasAuthority('role')"><a th:href="@{/role}"><i class="bi bi-circle"></i><span>角色管理</span> </a></li>
                                                                                                <li sec:authorize="hasAuthority('permission')"><a th:href="@{/permission}"><i class="bi bi-circle"></i><span>权限管理</span> </a></li>
                                                                                                <li> <a href="components-breadcrumbs.html"> <i class="bi bi-circle"></i><span>Breadcrumbs</span> </a></li>
                                                                                                <li> <a href="components-buttons.html"> <i class="bi bi-circle"></i><span>Buttons</span> </a></li>
                                                                                                <li> <a href="components-cards.html"> <i class="bi bi-circle"></i><span>Cards</span> </a></li>
                                                                                                <li> <a href="components-carousel.html"> <i class="bi bi-circle"></i><span>Carousel</span> </a></li>
                                                                                                <li> <a href="components-list-group.html"> <i class="bi bi-circle"></i><span>List group</span> </a></li>
                                                                                                <li> <a href="components-modal.html"> <i class="bi bi-circle"></i><span>Modal</span> </a></li>
                                                                                                <li> <a href="components-tabs.html"> <i class="bi bi-circle"></i><span>Tabs</span> </a></li>
                                                                                                <li> <a href="components-pagination.html"> <i class="bi bi-circle"></i><span>Pagination</span> </a></li>
                                                                                                <li> <a href="components-progress.html"> <i class="bi bi-circle"></i><span>Progress</span> </a></li>
                                                                                                <li> <a href="components-spinners.html"> <i class="bi bi-circle"></i><span>Spinners</span> </a></li>
                                                                                                <li> <a href="components-tooltips.html"> <i class="bi bi-circle"></i><span>Tooltips</span> </a></li>
                                                                                            </ul>
                                                                                        </li>
                                                                                        <li class="nav-item">
                                                                                            <a class="nav-link collapsed" data-bs-target="#forms-nav" data-bs-toggle="collapse" href="#"> <i class="bi bi-journal-text"></i><span>Forms</span><i class="bi bi-chevron-down ms-auto"></i> </a>
                                                                                            <ul id="forms-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
                                                                                                <li> <a href="forms-elements.html"> <i class="bi bi-circle"></i><span>Form Elements</span> </a></li>
                                                                                                <li> <a href="forms-layouts.html"> <i class="bi bi-circle"></i><span>Form Layouts</span> </a></li>
                                                                                                <li> <a href="forms-editors.html"> <i class="bi bi-circle"></i><span>Form Editors</span> </a></li>
                                                                                                <li> <a href="forms-validation.html"> <i class="bi bi-circle"></i><span>Form Validation</span> </a></li>
                                                                                            </ul>
                                                                                        </li>
                                                                                        <li class="nav-item">
                                                                                            <a class="nav-link collapsed" data-bs-target="#tables-nav" data-bs-toggle="collapse" href="#"> <i class="bi bi-layout-text-window-reverse"></i><span>Tables</span><i class="bi bi-chevron-down ms-auto"></i> </a>
                                                                                            <ul id="tables-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
                                                                                                <li> <a href="tables-general.html"> <i class="bi bi-circle"></i><span>General Tables</span> </a></li>
                                                                                                <li> <a href="tables-data.html"> <i class="bi bi-circle"></i><span>Data Tables</span> </a></li>
                                                                                            </ul>
                                                                                        </li>
                                                                                        <li class="nav-item">
                                                                                            <a class="nav-link collapsed" data-bs-target="#charts-nav" data-bs-toggle="collapse" href="#"> <i class="bi bi-bar-chart"></i><span>Charts</span><i class="bi bi-chevron-down ms-auto"></i> </a>
                                                                                            <ul id="charts-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
                                                                                                <li> <a href="charts-chartjs.html"> <i class="bi bi-circle"></i><span>Chart.js</span> </a></li>
                                                                                                <li> <a href="charts-apexcharts.html"> <i class="bi bi-circle"></i><span>ApexCharts</span> </a></li>
                                                                                                <li> <a href="charts-echarts.html"> <i class="bi bi-circle"></i><span>ECharts</span> </a></li>
                                                                                            </ul>
                                                                                        </li>
                                                                                        <li class="nav-item">
                                                                                            <a class="nav-link collapsed" data-bs-target="#icons-nav" data-bs-toggle="collapse" href="#"> <i class="bi bi-gem"></i><span>Icons</span><i class="bi bi-chevron-down ms-auto"></i> </a>
                                                                                            <ul id="icons-nav" class="nav-content collapse " data-bs-parent="#sidebar-nav">
                                                                                                <li> <a href="icons-bootstrap.html"> <i class="bi bi-circle"></i><span>Bootstrap Icons</span> </a></li>
                                                                                                <li> <a href="icons-remix.html"> <i class="bi bi-circle"></i><span>Remix Icons</span> </a></li>
                                                                                                <li> <a href="icons-boxicons.html"> <i class="bi bi-circle"></i><span>Boxicons</span> </a></li>
                                                                                            </ul>
                                                                                        </li>
                                                                                        <li class="nav-heading">Pages</li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="users-profile.html"> <i class="bi bi-person"></i> <span>Profile</span> </a></li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="pages-faq.html"> <i class="bi bi-question-circle"></i> <span>F.A.Q</span> </a></li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="pages-contact.html"> <i class="bi bi-envelope"></i> <span>Contact</span> </a></li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="pages-register.html"> <i class="bi bi-card-list"></i> <span>Register</span> </a></li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="pages-login.html"> <i class="bi bi-box-arrow-in-right"></i> <span>Login</span> </a></li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="pages-error-404.html"> <i class="bi bi-dash-circle"></i> <span>Error 404</span> </a></li>
                                                                                        <li class="nav-item"> <a class="nav-link collapsed" href="pages-blank.html"> <i class="bi bi-file-earmark"></i> <span>Blank</span> </a></li>
                                                                                    </ul>
                                                                                </aside>
                                                                            </body>
                                                                            </html>

list_user.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="utf-8">
        <meta content="width=device-width, initial-scale=1.0" name="viewport">
        <title>Dashboard - Admin Bootstrap Template</title>
        <meta name="robots" content="noindex, nofollow">
        <meta content="" name="description">
        <meta content="" name="keywords">
        <link href="/assets/img/favicon.png" rel="icon">
        <link href="/assets/img/apple-touch-icon.png" rel="apple-touch-icon">
        <link href="/assets/css/bootstrap.min.css" rel="stylesheet">
        <link href="/assets/css/bootstrap-icons.css" rel="stylesheet">
        <link href="/assets/css/boxicons.min.css" rel="stylesheet">
        <link href="/assets/css/quill.snow.css" rel="stylesheet">
        <link href="/assets/css/quill.bubble.css" rel="stylesheet">
        <link href="/assets/css/remixicon.css" rel="stylesheet">
        <link href="/assets/css/simple-datatables.css" rel="stylesheet">
        <link href="/assets/css/style.css" rel="stylesheet">
    </head>
    <body>
        <header id="header" class="header fixed-top d-flex align-items-center" th:replace="fragments/header :: header">
        </header>
        <aside id="sidebar" class="sidebar" th:replace="fragments/sidebar :: sidebar">
        </aside>
        <main id="main" class="main">
            <div class="pagetitle">
                <nav>
                    <ol class="breadcrumb">
                        <li class="breadcrumb-item"><a href="index.html">主页</a></li>
                        <li class="breadcrumb-item active">用户管理</li>
                    </ol>
                </nav>
            </div>
            <section class="section dashboard">
                <div class="row">
                    <div class="col-lg-12">
                        <div class="card">
                            <div class="card-body">

                                <br/>
                                <br/>
                                <div class="table-title">
                                    <div class="row">
                                        <div class="col-sm-6">
                                            <h2> <b>用户管理</b></h2>
                                        </div>
                                        <div class="col-sm-6">
                                            <div sec:authorize="hasAnyAuthority('permission_create')">
                                                <a href="/user/new" class="btn btn-success" ><i class="bi bi-person-plus" data-toggle="tooltip" title="添加用户"></i></a>
                                                <!-- <a href="#deleteEmployeeModal" class="btn btn-danger" data-toggle="modal"><i class="material-icons">&#xE15C;</i> <span>Delete</span></a>						 -->
                                            </div>
                                        </div>
                                    </div>
                                </div>
                                <table class="table table-striped table-hover">
                                    <thead >
                                        <tr>
                                            <th>ID</th>
                                            <th>User Name</th>
                                            <th>E-mail</th>
                                            <th>Name</th>
                                            <th>Home Page</th>
                                            <th>Roles</th>
                                            <th>Actions</th>
                                        </tr>
                                    </thead>
                                    <tbody>
                                        <tr th:each="user: ${listUsers}">
                                            <td th:text="${user.id}">User ID</td>
                                            <td th:text="${user.username}">User Name</td>
                                            <td th:text="${user.email}">E-mail</td>
                                            <td th:text="${user.name}">Name</td>
                                            <td th:text="${user.homepage}">Home Page</td>
                                            <td th:text="${user.roles}">Roles</td>
                                            <td>
                                                <a sec:authorize="hasAuthority('user_edit')" th:href="@{'/user/edit/' + ${user.id}}" class="edit" ><i class="bi bi-pencil" data-toggle="tooltip" title="编辑"></i></a>
                                                <a sec:authorize="hasAuthority('user_edit')" th:href="@{'/user/resetpassword/' + ${user.id}}" class="reset" ><i class="bi bi-key" data-toggle="tooltip" title="重置密码"></i></a>
                                                <a sec:authorize="hasAuthority('user_delete')" th:href="@{'/user/delete/' + ${user.id}}" class="delete" ><i class="bi bi-trash" data-toggle="tooltip" title="删除"></i></a>


                                            </td>
                                        </tr>
                                    </tbody>
                                </table>



                            </div>
                        </div>
                    </div>
            </section>
        </main>
        <footer id="footer" class="footer" th:replace="fragments/footer :: footer">
            <div class="copyright"> &copy; Copyright <strong><span>Compnay Name</span></strong>. All Rights Reserved</div>
            <div class="credits"> with love <a href="https://freeetemplates.com/">FreeeTemplates</a></div>
        </footer>
        <a href="#" class="back-to-top d-flex align-items-center justify-content-center"><i class="bi bi-arrow-up-short"></i></a>
        <script src="/assets/js/apexcharts.min.js"></script>
        <script src="/assets/js/bootstrap.bundle.min.js"></script>
        <script src="/assets/js/chart.min.js"></script>
        <script src="/assets/js/echarts.min.js"></script>
        <script src="/assets/js/quill.min.js"></script>
        <script src="/assets/js/simple-datatables.js"></script>
        <script src="/assets/js/tinymce.min.js"></script>
        <script src="/assets/js/validate.js"></script>
        <script src="/assets/js/main.js"></script>

    </body>
</html>

 

履歴パスワードの保存と再利用の検出

package com.example;

import org.apache.commons.text.similarity.LevenshteinDistance;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;

import jakarta.validation.Valid;
import java.util.Date;
import java.util.List;
import java.util.Set;

@Controller

public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @GetMapping("/user/new")
    public String showRegistrationForm(Model model) {
        model.addAttribute("user", new UserDTO());

        return "user/new_user";
    }

    @GetMapping("/user")
    public String listUsers(Model model) {
        List<User> listUsers = userRepository.findAll();
        model.addAttribute("listUsers", listUsers);

        return "user/list_user";
    }

    @GetMapping("/user/edit/{id}")
    public String editUser(@PathVariable("id") Long id, Model model) {
        User user = userService.get(id);
        List<Role> listRoles = userService.listRoles();
        model.addAttribute("user", user);
        model.addAttribute("listRoles", listRoles);
        return "user/edit_user";
    }

    @PostMapping("/user/save")
    public String saveUser(@Valid @ModelAttribute("user") UserDTO user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "user/new_user";
        }
        userService.saveUser(user);

        return "redirect:/user";
    }

    @PostMapping("/user/update")
    public String updateUser(User user) {
        User repoUser = userRepository.findById(user.getId()).orElse(null);

        if (repoUser != null) {
            repoUser.setUsername(user.getUsername());
            repoUser.setEmail(user.getEmail());
            repoUser.setName(user.getName());
            repoUser.setEnabled(user.getEnabled());
            repoUser.setAccountNonLocked(user.getAccountNonLocked());
            repoUser.setHomepage(user.getHomepage());
            repoUser.setRoles(user.getRoles());
            userRepository.save(repoUser);
        }

        return "redirect:/user";
    }

    @GetMapping("/user/resetpassword/{id}")
    public String showResetPasswordForm(@PathVariable("id") Long id, Model model) {
        User user = userRepository.findById(id).orElse(null);
        UserResetPasswordDTO userResetPasswordDTO = new UserResetPasswordDTO();
        if (user != null) {
            userResetPasswordDTO.setId(user.getId());
            userResetPasswordDTO.setUsername(user.getUsername());
            model.addAttribute("user", userResetPasswordDTO);
            return "user/reset_password";
        }
        return "redirect:/user";
    }

    @PostMapping("/user/savepassword")
    public String savePassword(@Valid @ModelAttribute("user") UserResetPasswordDTO user, BindingResult bindingResult) {
        if (bindingResult.hasErrors()) {
            return "user/reset_password";
        }
        User repoUser = userRepository.findById(user.getId()).orElse(null);

        if (repoUser != null) {
            repoUser.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
            Date passwordChangedTime = new Date();

            repoUser.setPasswordChangedTime(passwordChangedTime);
            userRepository.save(repoUser);
        }

        return "redirect:/user";
    }

    @RequestMapping("/user/delete/{id}")
    public String deleteUser(@PathVariable(name = "id") Long id) {
        userRepository.deleteById(id);

        return "redirect:/user";
    }

    @GetMapping("user/info")
    public String userProfile(Model model) {

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        System.out.println(auth.getName());
        User user = userRepository.getByUsername(auth.getName());
        model.addAttribute("user", user);

        return "user/user_profile";
    }

    @GetMapping("/change/password")

    public String changePassword(Model model) {

        model.addAttribute("userDTO", new UserChangePasswordDTO());
        return "user/password_update";

    }

    @PostMapping("/new/password")
    public String
            newPassword(@Valid @ModelAttribute("userDTO") UserChangePasswordDTO userChangePasswordDTO, BindingResult bindingResult, Model model) {
        if (bindingResult.hasErrors()) {
            return "user/password_update";

        }

        Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        if (userChangePasswordDTO.getNewPass().equals(userChangePasswordDTO.getConfirmPass())) {
            User user = userService.findUserByUsername(auth.getName());
            boolean status = userService.isPasswordValid(userChangePasswordDTO.getPassword(), user.getPassword());

            if (status) {

                LevenshteinDistance levenshteinWithThreshold = new LevenshteinDistance(3);
                // Returns -1 since the actual distance, 4, is higher than the threshold
                System.out.println("Levenshtein distance: " + levenshteinWithThreshold.apply(userChangePasswordDTO.getNewPass(), userChangePasswordDTO.getPassword()));
                if (levenshteinWithThreshold.apply(userChangePasswordDTO.getNewPass(), userChangePasswordDTO.getPassword()) == -1) {
                    Set<History> setHistorysCheck = user.getHistorys();
                    boolean check = true;
                    for (History hist : setHistorysCheck) {
                        System.out.print(hist.getPassword());
                        if (bCryptPasswordEncoder.matches(userChangePasswordDTO.getNewPass(), hist.getPassword())) {
                            check = false;
                            break;
                        }
                    }

                    if (check) {

                        System.out.println(user.getHistorys());
                        History history = new History();
                        System.out.println("userChangePasswordDTO.getNewPass()=" + userChangePasswordDTO.getNewPass());
                        history.setPassword(bCryptPasswordEncoder.encode(userChangePasswordDTO.getNewPass()));
                        System.out.println(history);
                        Set<History> setHistorys = user.getHistorys();
                        setHistorys.add(history);
                        user.setHistorys(setHistorys);
                        System.out.println(user.getHistorys());
                        userService.changePassword(user, userChangePasswordDTO);
                        return "login";
                    } else {
                        model.addAttribute("passMatched", "New password was same as history..!");
                        return "user/password_update";
                    }
                } else {
                    model.addAttribute("passMatched", "New password need 4 diff with Current password..!");
                    return "user/password_update";
                }
            } else {

                model.addAttribute("wrongPass", "Current password was wrong..!");
                return "user/password_update";
            }

        } else {
            model.addAttribute("passMatched", "Password doesn't matched..!");
            return "user/password_update";
        }

    }

}

 

 

 

 

結論は:

ここまでは、フォームベースの認証とデータベース内の資格情報を使用して Spring Boot アプリケーションを保護する方法を学習しました。Spring セキュリティを使用すると、ログインおよびログアウト機能の実装が非常に簡単かつ便利になることがわかります。便宜上、以下のサンプル プロジェクトをダウンロードできます。

出典:allwaysoft/ProductManagerUserDetailsS​​erviceAuditBoot3.0 · GitHub

おすすめ

転載: blog.csdn.net/allway2/article/details/128050891