[イニシアチブ-SpringBoot]掲示板-3を作成します.Spring Securityの適用


Spring Securityとは?


Spring securityは、認証と認可を重点的に提供するカスタマイズ可能な認証とアクセス制御フレームワークです.
Springベースのアプリケーションを保護するための事実上の基準です.

Spring安全特性


包括的で拡張性の高い
  • 認定および認定サポート
  • セッションをロック、クリック、サイト間偽造などの攻撃から保護
  • シリアルAPI統合
  • オプションの
  • Spring Web MVC統合
  • サンプルファイルパス

    2.依存性の追加


    file : build.gradle
    plugins {
    	id 'org.springframework.boot' version '2.5.3'
    	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    	id 'java'
    }
    
    group = 'com.rptp'
    version = '0.0.1-SNAPSHOT'
    sourceCompatibility = '1.8'
    
    repositories {
    	mavenCentral()
    }
    
    dependencies {
    	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    	implementation 'org.springframework.boot:spring-boot-starter-web'
    	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    	implementation 'org.springframework.boot:spring-boot-starter-validation'
      //==================add==================
    	implementation 'org.springframework.boot:spring-boot-starter-security'
    	implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
    	implementation 'org.springframework.security:spring-security-test'
     // ==================add==================
    
    	annotationProcessor "org.projectlombok:lombok"
    	compileOnly "org.projectlombok:lombok"
    
    
    	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    
    	testCompileOnly 'org.projectlombok:lombok:1.18.20'
    	testAnnotationProcessor 'org.projectlombok:lombok:1.18.20'
    
    	runtimeOnly 'mysql:mysql-connector-java'
    }
    
    test {
    	useJUnitPlatform()
    }
    

    3.セキュリティ構成の作成

    package com.rptp.rptpSpringBoot.common.security;
    
    import com.rptp.rptpSpringBoot.core.member.service.MemberService;
    import lombok.RequiredArgsConstructor;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.builders.WebSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
    
    @Configuration
    @EnableWebSecurity
    @RequiredArgsConstructor
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        private final MemberService memberService;
    
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**");
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers("/admin/**").hasRole("ADMIN")
                    .antMatchers("/**").permitAll()
                    .and()
                    .formLogin()
                    .loginPage("/login")
                    .defaultSuccessUrl("/login-success")
                    .permitAll()
                    .and()
                    .logout()
                    .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                    .logoutSuccessUrl("/logout-success")
                    .invalidateHttpSession(true)
                    .and()
                    .exceptionHandling().accessDeniedPage("/denied");
        }
    
        @Override
        public void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(memberService).passwordEncoder(passwordEncoder());
        }
    }

    コード詳細

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    復号クラスに注入されます.
    CryptPasswordEncoderは、Cryptハッシュ関数を使用してパスワードをエンコードする方法と、ユーザーがコミットしたパスワードがリポジトリに格納されているパスワードと一致するかどうかを決定する方法を提供します.
    ハッシュの強度は、コンストラクション関数のパラメータ値(Version、Stress、SecureRandom instance)によって調整できます.
     @Override
     public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/css/**", "/js/**", "/img/**", "/lib/**");
     }
    Webセキュリティは、FilterChainProxyを生成するフィルタです.
    antMatchersで指定したアドレスのファイルは認証を無視します.
    @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                     // (1)
                    .antMatchers("/admin/**").hasRole("ADMIN")
                    .antMatchers("/**").permitAll()
                    
                    
                     // (2)
                    .and()
                    .formLogin()
                    .loginPage("/login")
                    .defaultSuccessUrl("/login-success")
                    .permitAll()
                    
                    // (3)     
                    .and()
                    .logout()
                    .logoutRequestMatcher(new AntPathRequestMatcher("/logout"))
                    .logoutSuccessUrl("/logout-success")
                    .invalidateHttpSession(true)
                    
                     // (4)     
                    .and()
                    .exceptionHandling().accessDeniedPage("/denied");
        }
    HttpSecurityにより、HTTPリクエストに対してWebベースのセキュリティを構成することができる.
    (1). antMatchers()メソッドを使用して特定のパスを指定し、permitAll()メソッドとhasRole()メソッドを使用してロールに基づいてアクセス設定を設定します.
    (2). フォームベースの認証はformLogin()で行います.ログイン情報は基本的にHttpSessionを使用します.
    (3). ログアウトの方法をサポートします.WebSecurityコンフィギュレータアダプタを使用すると自動的に有効になります.
    デフォルトでは、/logoutにアクセスするとHTTPセッションが削除されます.
    (4). 処理403異常処理のハンドル.

    4.メンバーエンティティロールの追加


    4-1. Role enumの作成


    file : Role.java
    package com.rptp.rptpSpringBoot.core.member.domain;
    
    import lombok.AllArgsConstructor;
    import lombok.Getter;
    
    @AllArgsConstructor
    @Getter
    public enum Role {
        ADMIN("ROLE_ADMIN"),
        MEMBER("ROLE_MEMBER"),
        GUEST("ROLE_GUEST");
    
        private String value;
    }

    4-2. メンバーの変更


    file : Member.java
    package com.rptp.rptpSpringBoot.core.member.domain;
    
    import lombok.Builder;
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    
    import javax.persistence.*;
    
    @Entity
    @Getter
    @Setter
    @NoArgsConstructor
    public class Member {
    
        @Id
        @GeneratedValue(strategy = GenerationType.IDENTITY)
        private Long memberId;
    
        @Column(nullable = false)
        private String name;
    
        @Column(nullable = false)
        private String password;
    
        private String profilePhoto;
    
        @Column(nullable = false)
        private String nickName;
        
    //==================add==================
        @Column(nullable = false)
        @Enumerated(EnumType.STRING)
        private Role role = Role.GUEST;
    //==================add==================
    
        @Builder
        public Member(String name, String password, String profilePhoto, String nickName) {
            this.name = name;
            this.password = password;
            this.profilePhoto = profilePhoto;
            this.nickName = nickName;
        }
    }

    5.MemberRepository findByNameメソッドの追加


    file : MemberRepository.java
    package com.rptp.rptpSpringBoot.core.member.domain;
    
    import org.springframework.data.jpa.repository.JpaRepository;
    
    import java.util.Optional;
    
    public interface MemberRepository extends JpaRepository<Member, Long> {
    //==================add==================
        Optional<Member> findByName(String name);
    //==================add==================
    }

    6.メンバーサービスでのUserDetailServiceインタフェースの実装


    file : MemberService.java
    package com.rptp.rptpSpringBoot.core.member.service;
    
    import com.rptp.rptpSpringBoot.core.member.domain.Member;
    import com.rptp.rptpSpringBoot.core.member.domain.MemberRepository;
    import com.rptp.rptpSpringBoot.core.member.domain.Role;
    import com.rptp.rptpSpringBoot.core.member.dto.SignUpRequest;
    import lombok.RequiredArgsConstructor;
    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.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    import org.springframework.stereotype.Service;
    
    
    import org.springframework.transaction.annotation.Transactional;
    
    import java.util.ArrayList;
    import java.util.List;
    import java.util.Optional;
    
    @Service
    @RequiredArgsConstructor
    //==================edit==================
    public class MemberService implements UserDetailsService {
    //==================edit==================
        private final MemberRepository memberRepository;
    
        private BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
    
        @Transactional
        public Long signUp(SignUpRequest req) {
            return  memberRepository.save(buildMember(req)).getMemberId();
        }
    
        private Member buildMember(SignUpRequest req) {
            return Member.builder()
                    .name(req.getName())
            //==================edit==================
      		.password(passwordEncoder.encode(req.getPassword()))
            //==================edit==================
                    .profilePhoto(req.getProfilePhoto())
                    .nickName(req.getNickName())
                    .build();
        }
    //==================add==================
        @Override
        public UserDetails loadUserByUsername(String name) throws UsernameNotFoundException {
            Member member = memberRepository.findByName(name)
                    .orElseThrow(() -> new UsernameNotFoundException(name + "은 존재하지 않습니다"));
    
            List<GrantedAuthority> authorities = new ArrayList<>();
    
            if (member.getRole() == Role.ADMIN) {
                authorities.add(new SimpleGrantedAuthority(Role.ADMIN.getValue()));
            }
    
            authorities.add(new SimpleGrantedAuthority(Role.MEMBER.getValue()));
    
            return new User(member.getName(), member.getPassword(), authorities);
        }
        //==================add==================
    
    }

    7.ビューの追加&ハンドルの変更と追加


    admin.html
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml"
                xmlns:th="http://www.thymeleaf.org">
    <head>
      <meta charset="UTF-8">
      <title>어드민</title>
    </head>
    <body>
    <h1>어드민 페이지입니다.</h1>
    <hr>
    </body>
    </html>
    demied.html
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml"
          xmlns:th="http://www.thymeleaf.org">
    <head>
      <meta charset="UTF-8">
      <title>접근 거부</title>
    </head>
    <body>
    <h1>접근 불가 페이지입니다.</h1>
    <hr>
    </body>
    </html>
    index.html
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml"
          xmlns:th="http://www.thymeleaf.org"
          xmlns:sec="http://www.w3.org/1999/xhtml">
    <head>
        <meta charset="UTF-8">
        <title>RPTP</title>
    </head>
    <body>
    <h1>메인 페이지</h1>
    <hr>
    <a sec:authorize="isAnonymous()" th:href="@{/login}">로그인</a>
    <a sec:authorize="isAuthenticated()" th:href="@{/logout}">로그아웃</a>
    <a sec:authorize="isAnonymous()" th:href="@{/sign-up}">회원가입</a>
    <a sec:authorize="hasRole('ROLE_MEMBER')" th:href="@{/user}">내정보</a>
    <a sec:authorize="hasRole('ROLE_ADMIN')" th:href="@{/admin}">어드민</a>
    </body>
    </html>
    sec:authorizerでログインユーザーの状態に応じてラベルを表示するかどうかを決定します.
    login.html
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
          xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
      <title>Spring Security Example </title>
    </head>
    <body>
    <div th:if="${param.error}">
      유효하지 않은 아이디 또는 비밀번호입니다
    </div>
    <div th:if="${param.logout}">
      로그아웃
    </div>
    <form th:action="@{/login}" method="post">
      
      <!--input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /-->
      <div><label> User Name : <input type="text" name="username"/> </label></div>
      <div><label> Password: <input type="password" name="password"/> </label></div>
      <div><input type="submit" value="로그인"/></div>
    </form>
    </body>
    </html>
    Spring Securityが適用される場合、POST方式で送信されるすべてのデータはcsrfトークン値を必要とする.
    したがって本来はフォーム上でcsrfトークンを一緒に送信すべきであるが、time lifeのth:actionを使用すると自動的にcsrfが送信される
    login-success.html
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml"
          xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>RPTP</title>
    </head>
    <body>
    로그인 완료!
    <a th:href="@{/}">메인으로</a>
    </body>
    </html>
    logout.html
    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.w3.org/1999/xhtml">
    <head>
      <meta charset="UTF-8">
      <title>로그아웃</title>
    </head>
    <body>
    <h1>로그아웃 처리되었습니다.</h1>
    <hr>
    <a th:href="@{'/'}">메인으로 이동</a>
    </body>
    </html>
    logout-success.html
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml"
          xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>RPTP</title>
    </head>
    <body>
    로그아웃 완료!
    <a th:href="@{/}">메인으로</a>
    </body>
    </html>
    sign-up.html
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml"
          xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>RPTP</title>
    </head>
    <body>
    <form th:action="@{/api/member}" method="post">
        <label>아이디<input type="text" name="name"></label>
        <label>password<input type="password" name="password"></label>
        <label>프로필사진<input type="text" name="profilePhoto"></label>
        <label>별명<input type="text" name="nickName"></label>
        <input type="submit" value="가입하기">
    </form>
    
    </body>
    </html>
    sign-up-success.html
    <!DOCTYPE html>
    <html xmlns="http://www.w3.org/1999/xhtml"
          xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>RPTP</title>
    </head>
    <body>
    회원가입 완료!
    <a th:href="@{/}">메인으로</a>
    </body>
    </html>
    MainController.java
    package com.rptp.rptpSpringBoot.api;
    
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    @RequestMapping("/")
    @RequiredArgsConstructor
    public class MainController {
    
        @GetMapping("")
        public String index(){
            return "/index";
        }
    
        @GetMapping("sign-up")
        public String signUp() {
            return "/sign-up";
        }
    
        @GetMapping("sign-up-success")
        public String signUpSuccess() {
            return "/sign-up-success";
        }
    
        @GetMapping("login")
        public String login() {
            return "/login";
        }
    
        @GetMapping("login-success")
        public String loginSuccess() {
            return "/login-success";
        }
    
        @GetMapping("logout-success")
        public String logoutSuccess() {
            return "/logout-success";
        }
    
        @GetMapping("denied")
        public String denied() {
            return "/denied";
        }
    
        @GetMapping("/admin")
        public String admin() {
            return "/admin";
        }
    
    }

    実行結果


    初回接続時のホームページ



    会員収入



    会員加入完了



    ログイン



    ログイン完了



    一般ユーザーのホームページ



    ログアウトページ



    オペレータホーム-ROLEをdbからADMINに直接変更



    リファレンス


    https://mangkyu.tistory.com/76
    https://victorydntmd.tistory.com/328
    https://kimvampa.tistory.com/129