實際要來看一下我們要使用個人的使用者資訊資料表要如何串接,因為 Security 提供很多客製化的介面所以需要實作許多特殊的物件就會讓流程蠻複雜的,整體來說可以先有個概念,我們原本需要透過 dao 來和 DB 溝通的部分需要改成 Security 框架的模式,原本可以由 Spring Data JPA 直接取得 User 的物件回傳,但因為 Security 將使用者資訊會封裝成 UserDetails 這個物件來回傳,必須要透過 UserDetailsService 這邊來取得這個物件,所以會想辦法把這些東西去實做出來。
資料庫是串接 MySQL ,相關配置可以參考 Spring Data JPA (1)基礎應用架構  引入相關 Dependency
資料庫架構 Database Schema 提供 SQL 給大家參考
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 CREATE  TABLE  users(     id         BIGINT  PRIMARY  KEY AUTO_INCREMENT,     username   VARCHAR (50 )  NOT  NULL  UNIQUE ,     email      VARCHAR (100 ) NOT  NULL  UNIQUE ,     password   VARCHAR (255 ) NOT  NULL ,     created_at DATETIME DEFAULT  CURRENT_TIMESTAMP ,     updated_at DATETIME DEFAULT  CURRENT_TIMESTAMP  ON  UPDATE  CURRENT_TIMESTAMP  ); CREATE  TABLE  roles(     id        BIGINT  PRIMARY  KEY AUTO_INCREMENT,     role_name VARCHAR (255 ) NOT  NULL  UNIQUE  ); CREATE  TABLE  user_roles(     user_id BIGINT  NOT  NULL ,     role_id BIGINT  NOT  NULL ,     PRIMARY  KEY (user_id, role_id),     FOREIGN  KEY (user_id) REFERENCES  users (id),     FOREIGN  KEY (role_id) REFERENCES  roles (id) ); 
Config 配置 延續前面文章的配置
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @EnableWebSecurity @Configuration public  class  SecurityConfig  {    @Bean      public  SecurityFilterChain filterChain (HttpSecurity httpSecurity)  throws  Exception {         return  httpSecurity                 .csrf(customizer -> customizer.disable())                 .authorizeHttpRequests((registry) -> registry                         .requestMatchers(HttpMethod.POST, "/register" , "/login" ).permitAll()                         .requestMatchers(HttpMethod.GET, "/error" , "/api/products/**" ).permitAll()                         .requestMatchers(HttpMethod.GET, "/checkAuthentication" ).hasAnyAuthority("ROLE_BUYER" , "ROLE_SELLER" , "ROLE_ADMIN" )                         .requestMatchers("/api/users/**" ).hasAuthority("ROLE_ADMIN" )                         .anyRequest().authenticated()                 )                 .formLogin(Customizer.withDefaults())                 .build();     } 
建 User, Role , UserRole, UserRepository 權限部分需要在 users 表裡面多一個 role 的欄位來進行多對多關聯,並且建立 roles 表和 user_roles 表
User Entity
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 @Entity @Data @Table(name = "users") public  class  User  extends  BaseEntity {    @Column(nullable = false)      private  String username;     @Column(nullable = false)      private  String password;     @Column(nullable = false)      private  String email;     @ManyToMany(fetch = FetchType.EAGER)      @JoinTable(              name = "user_roles",             joinColumns = @JoinColumn(name = "user_id"),             inverseJoinColumns = @JoinColumn(name = "role_id"),             uniqueConstraints = @UniqueConstraint(columnNames = {"user_id", "role_id"})     )     private  Set<Role> roles = new  HashSet <>(); } 
Role Entity
roles 表內 roleName 的欄位用 Enum 作為資料類型,所以另外有 UserRole 的 Enum
1 2 3 4 5 6 7 8 9 10 11 12 13 @Data @Entity @Table(name = "roles") public  class  Role  {    @Id      @GeneratedValue(strategy = GenerationType.IDENTITY)      private  Integer id;     @Enumerated(EnumType.STRING)      @Column(nullable = false, unique = true)      private  UserRole roleName; } 
UserRole Enum
三種權限分別是 ADMIN, BUYER, SELLER 分別是管理者、買家、商家。
1 2 3 4 5 6 7 package  com.oseanchen.crudproject.model;public  enum  UserRole  {    ROLE_ADMIN,     ROLE_BUYER,     ROLE_SELLER } 
UserPricipal 實作 (Custom UserDetails) 啟動程式時,Spring Security 會檢查專案中是否有 UserDetailsService 。如果沒有,則自動建立一個前一篇有提到的預設管理元件 InMemoryUserDetailsManager ,並提供預設 user 然後把密碼印在 console 。如果我們自行提供 UserDetailsService ,則 Security 就會自動採用。
把我們 User Entity 轉成 Security 的 UserDetails 介面,我們取名 UserPrincipal 來稱呼你也可以自訂自己的 UserDetails 名稱,由於是介面所以它所提供的方法都要實做一遍。
裡面有幾個主要的就是 getUsername, getPassword, getAuthorities ,讓我們可以拿到我們要的 User 資訊,其他的設置主要關於帳號是否到期、鎖定、密碼過期、啟用等等,可以依據你的需求配置紀錄相關資訊,讓 Spring Security 可以進行驗證,就算帳號密碼正確也不一定可以通過認證。這邊先進行簡單的設計都預設為 true 通過驗證
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 public  class  UserPrincipal  implements  UserDetails  {    private  User user;     public  UserPrincipal (User user)  {         this .user = user;     }     @Override      public  Collection<? extends  GrantedAuthority > getAuthorities() {         Set<GrantedAuthority> authorities = user.getRoles().stream()                 .map(role -> new  SimpleGrantedAuthority (role.getRoleName().name()))                 .collect(Collectors.toSet());         return  authorities;     }     @Override      public  String getPassword ()  {         return  user.getPassword();     }     @Override      public  String getUsername ()  {         return  user.getUsername();     }     @Override      public  boolean  isAccountNonExpired ()  {         return  true ;     }     @Override      public  boolean  isAccountNonLocked ()  {         return  true ;     }     @Override      public  boolean  isCredentialsNonExpired ()  {         return  true ;     }     @Override      public  boolean  isEnabled ()  {         return  true ;     } } 
UserDetailService 實作 (Custom UserDetailService) 接著需要加入管理的部分, 先前有提到原本預設管理元件 InMemoryUserDetailsManager 它其實就會去實作 UserDetailService,裡面有一個方法是 loadUserByUsername 需要實作,這邊需要實作用 username 來找出 User 資料,然後把 User 封裝成 UserDetails
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @Service public  class  MyUserDetailsService  implements  UserDetailsService  {    @Autowired      private  UserDao repo;     @Override      public  UserDetails loadUserByUsername (String username)  throws  UsernameNotFoundException {         User  user  =  repo.findByUsername(username);         if  (user == null ) {             System.out.println("username = "  + username + " not found" );             throw  new  UsernameNotFoundException ("user not found" );         }         return  new  UserPrincipal (user);     } } 
UserController, UserService 實作確認可創建及撈出資料 建立 UserController 的幾個主要端口讓 user 可以先創建跟查詢,這邊我們透過 userRequest 的 DTO 來傳遞註冊時的帳號密碼等資訊
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 @RestController @RequestMapping("/api") public  class  UserController  {    @Autowired      private  UserService userService;     @GetMapping("/users")      public  ResponseEntity<List<User>> getUsers ()  {         List<User> userList = userService.getUsers();         return  ResponseEntity.status(HttpStatus.OK).body(userList);     }     @GetMapping("/users/{id}")      public  ResponseEntity<Optional<User>> getUser (@PathVariable  Integer id)  {         Optional<User> user = userService.getUserByID(id);         if  (user.isPresent()) {             return  ResponseEntity.status(HttpStatus.OK).body(user);         }         return  ResponseEntity.status(HttpStatus.NOT_FOUND).build();     }     @PostMapping("/register")      public  ResponseEntity<User> Register (@RequestBody  @Valid  UserRequest userRequest)  {         User  createdUser  =  userService.createUser(userRequest);         return  ResponseEntity.status(HttpStatus.CREATED).body(createdUser);     } } 
Service 這邊主要處理資料轉換,註冊的部分要 save user 預設先用一個 set 裝入預設的角色,我們是設定一般辦好會有買家跟賣家的權限,所以從 UserRole Enum 裡面取得放入。 
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 @Service public  class  UserService  {    @Autowired      private  UserDao userDao;     @Autowired      private  RoleDao roleDao;     public  List<User> getUsers ()  {         return  userDao.findAll();     }     public  Optional<User> getUserByID (Integer id)  {         return  userDao.findById(id);     }     @Transactional      public  User createUser (UserRequest userRequest)  {         Set<Role> roles = new  HashSet <>();         List<String> roleList = Arrays.asList(UserRole.ROLE_BUYER.name(), UserRole.ROLE_SELLER.name());         User  user  =  convertToModel(userRequest);         user.getRoles().addAll(roles);         return  userDao.save(user);     }     private  User convertToModel (UserRequest userRequest)  {         User  user  =  new  User ();         user.setUsername(userRequest.getUsername());         user.setEmail(userRequest.getEmail());         user.setPassword(userRequest.getPassword());         return  user;     } } 
UserRequest DTO
DTO 內應用 @Valid 相關的驗證註解設定一些對應的參數規格
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 package  com.oseanchen.crudproject.dto;import  jakarta.validation.constraints.*;import  lombok.Data;@Data public  class  UserRequest  {    @NotBlank(message = "username can not be blank")      private  String username;     @NotBlank(message = "password can not be blank")      @Size(min = 6, message = "password length at least 6 characters")      private  String password;     @NotBlank(message = "email can not be blank")      @Email(message = "invalid email format")      private  String email; } 
建立 PasswordEncoder 提升資安規格 使用 Bcrypt 加密方式,比起 MD5 或是 SHA 的雜湊演算更安全,因為加入隨機生成的鹽值 salt 可以防止駭客使用彩虹表來進行破解。
Service 引入 BCryptPasswordEncoder 引入後將準備儲存的密碼進行加密。
1 2 3 4 5 6 7 8 9 10 11 12 private  BCryptPasswordEncoder  encoder  =  new  BCryptPasswordEncoder (12 );    @Transactional      public  User createUser (UserRequest userRequest)  {         Set<Role> roles = new  HashSet <>();         List<UserRole> roleList = Arrays.asList(UserRole.ROLE_BUYER, UserRole.ROLE_SELLER);                  userRequest.setPassword(encoder.encode(userRequest.getPassword()));         User  user  =  convertToModel(userRequest);         user.getRoles().addAll(roles);         return  userDao.save(user);     } 
最後可以測試目前儲存的資料,假設我們放入下面註冊的資料。
1 2 3 4 5 6 7 8 9 10 11 12 {     "username"  :  "sean" ,      "password"  :  "sean123" ,      "email" : "kim@123.com"  } {     "username"  :  "tom" ,      "password"  :  "tom123" ,      "email" : "tom@123.com"  } 
資料庫儲存時應該看到的資料像是下面這樣,密碼會是加密過的。
Config 改變 AuthenticationProvider Encoder 方式 這邊會透過 DaoAuthenticationProvider 重新設定 AuthenticationProvider 所使用的 userDetailsService, 還有 passwordEncoder
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 @EnableWebSecurity @Configuration public  class  SecurityConfig  {		@Autowired      private  UserDetailsService userDetailsService;     @Bean      public  AuthenticationProvider authProvider ()  {         DaoAuthenticationProvider  provider  =  new  DaoAuthenticationProvider ();         provider.setUserDetailsService(userDetailsService);         provider.setPasswordEncoder(new  BCryptPasswordEncoder (12 ));         return  provider;     }     @Bean      public  SecurityFilterChain filterChain (HttpSecurity httpSecurity)  throws  Exception { 						     } 
實現登入 最後直接啟動程式連到 http://localhost:8080/login  確認可以正常登入,且登入之後可以根據我們 config 配置的權限進行瀏覽。
例如我上面 Config 配置只讓 ADMIN 的使用者可以查詢所有 user 的資料,如果是具有該權限的就可以成功瀏覽,但是其他使用者查詢就會是 403 Forbidden
先使用 sean 登入
再使用 tom 登入
總結: 以上就完成串接自己資料庫並且實現 Security 基本驗證的框架示範,總結一下這部分因為太多步驟要處理:
建立資料庫 schema, Entity、串連 Spring Data JPA 和資料庫 
UserDetails 介面實作 
UserDetailsService 介面實作撈出 user 並且封裝成 UserDetails 回傳 
建置 Controller 及 Service 測試 
加入 BCryptPasswordEncoder 實作,引入註冊帳號加密密碼後儲存、Config 重設 AuthenticationProvider 加入對應的 BCryptPasswordEncoder 
 
希望大家對於 Spring Security 有更多的認識,也可以自己建立起保護 server 的環境喔! 下一篇會引入現今業界常使用到的 JWT 方式來進行登入及相關權限的管控。
參考資料: