2020 年 5 月 14 日

IT Skills 波林

Polin WEI – 資訊工作者的技術手札

Spring Boot 客製化 登入 ( Login ) 與 認證 (Authenticate) 機制

2 min read
國際網球館

Spring Boot 客製化 登入 ( Login ) 與 認證 (Authenticate) 機制

 

一個網站基本的除了有前台的頁面展示外,還必需要有後台的管理機制,在官方網站有一篇文章:Securing a Web Application 在說明如何撰寫登入認證機制。之前在 使用 Eclipse 快速建立 Spring Starter Project 我們已經快速建立了一個網站,接下來將要對它加入登入的認證機制,並且將利用 AdminLTE3 來美化頁面, 預計需要有下面三個頁面,而這些頁面將使用 Freemarker 的模版引擎來改寫。

  1. 第一個頁面登入前的首頁 home.html:使用 pages/layout/top-nav.html 這個頁面
  2. 第二個是登入頁面 login.html:利用它作好的 pages/examples/login.html 頁面來作為登入的頁面。
  3. 第三個則是認證後的首頁 auth-home.html:使用 pages/layout/fixed-topnav.html 這個頁面。

 

  1. 系統操作劃面
  2. 設定 View : 首頁及登入頁面
  3. 實作 UserDetailsService
  4. WebSecurityConfig 客製化登入系統流程的設置
  5. 控制器 Controller: MainController
  6. 複制 AdminLTE3 必要的檔案到 src/main/resources 的 static 目錄下
  7. 登入與認證後頁面

 

先來看完成後的劃面長怎樣,登入系統首頁時

auth_home

按下 Login 時,轉到登入頁面

auth_login

依照登入的帳號不同,顯示登入者帳號,以 admin 登入時

auth_success_admin

以一般使用者帳號登入時

auth_success_user

 

  • 設定 View : 首頁及登入頁面

大致上的登入劃面瞭解後,就開始來實作程式,首先設定首頁及登入頁面:

@Configuration
public class MvcConfig implements WebMvcConfigurer {
 
 private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
 public void addViewControllers(ViewControllerRegistry registry) {
     registry.addViewController("/home").setViewName("home");
     registry.addViewController("/").setViewName("home");
     registry.addViewController("/login").setViewName("login");
 }

}

 

  • 實作 class AppUserDetailsService implements UserDetailsService 來替換預先的認證方法:

Spring Boot 在認證 spring-boot-starter-security 裡的有預留客製化的接口,可以讓您客製自已想要的認證方法,在這裡先建立一個 Class: AppUserDetailsService ,並在此預設好兩組固定的帳號與密碼,以便後面驗證測試。

@Service
public class AppUserDetailsService implements UserDetailsService {
 private final Logger logger = LoggerFactory.getLogger(this.getClass());

 private static List<UserObject> users = new ArrayList(); 
 
 public AppUserDetailsService() {
  users.add(new UserObject("admin", "123", "ADMIN"));
        users.add(new UserObject("polinwei", "234", "USER"));
 }

 @Override
 public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
  Optional<UserObject> user = users.stream()
                .filter(u -> u.name.equals(username))
                .findAny();
  if (!user.isPresent()) {
  throw new UsernameNotFoundException("User not found by name: " + username);
  }
  return toUserDetails(user.get());
 }
 
    private static class UserObject {
        private String name;
        private String password;
        private String role;
        BCryptPasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

        public UserObject(String name, String password, String role) {
            this.name = name;
            this.password = passwordEncoder.encode(password);
            this.role = role;
        }
    }   
 
    private UserDetails toUserDetails(UserObject userObject) {      
  
        return User.withUsername(userObject.name)
                   .password(userObject.password)
                   .roles(userObject.role).build();
    }
    
}

上面的程式中, UserObject 的密碼需要經過 BCryptPasswordEncoder 方法來作加密,否則在認證的過程中會出現 Encoded password does not look like BCrypt 的錯誤。

 

  • WebSecurityConfig 客製化登入系統流程的設置

這裡主要是設定那些頁面是不需要認證就可以查詢,資源目錄如:fonts 及 AdminLTE3 目錄下所有的檔案及目錄。以及登入與認證成功後頁面的 URL

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
 
 private final Logger logger = LoggerFactory.getLogger(this.getClass());
 
 @Autowired
 AppUserDetailsService appUserDetailsService;   
 
 @Override
 protected void configure(AuthenticationManagerBuilder auth) throws Exception {
  auth
  .userDetailsService(appUserDetailsService)
  .passwordEncoder(new BCryptPasswordEncoder());
 }
 
    @Override
    protected void configure(HttpSecurity httpSecurity) throws Exception {
  httpSecurity
    .authorizeRequests() // 認證機制設置              
     .antMatchers("/", "/home").permitAll() // 首頁不需認證
     .anyRequest().authenticated() // 除了以上的 URL 外, 都需要認證才可以訪問
     .and()
    .formLogin()
     .loginPage("/login") // 認證頁面指向頁頁
     .defaultSuccessUrl("/auth/home")
     .permitAll()
     .and()
    .logout()
     .permitAll();

  // disable page caching
        httpSecurity
                .headers()
                .frameOptions().sameOrigin()
                .cacheControl();

 }
    
 @Override
 public void configure(WebSecurity web) throws Exception {
  //allow anonymous resource requests
  web.ignoring().antMatchers("/fonts/**","/AdminLTE3/**");
 } 

}

 

  • 控制器 Controller: MainController
@Controller
public class MainController {
 private final Logger logger = LoggerFactory.getLogger(this.getClass());
 private Authentication auth;

 @Autowired 
 UserDetailsService userDetailsService;

 @RequestMapping("/login")
 public String loginForm(HttpServletRequest request, Model model) {
  auth = SecurityContextHolder.getContext().getAuthentication();
  if (!auth.getPrincipal().equals("anonymousUser")) {
   return "auth-home";
  }
  return "login";
 }
  
 @RequestMapping("/auth/home")
 public String authHome(Model model) {
  auth = SecurityContextHolder.getContext().getAuthentication();
  model.addAttribute("username", auth.getName())
   .addAttribute("roles", auth.getAuthorities());
  return "auth-home";
 }
   
}

使用 Eclipse 快速建立 Spring Starter Project 一文有提到,建立專案時,專案預設的 package 是在 com.apps ,所以 MainController 的 package 需要在 com.apps 下,如本例為 com.apps.controller

 

  • 複制 AdminLTE3 必要的檔案到 src/main/resources 的 static 目錄下

Spring Boot 在 Eclipse 的安裝與設定 中說的,下載 Web Page Layout – AdminLTE3https://github.com/ColorlibHQ/AdminLTE/releases 並只要複制 dist & plugins 這兩個目錄即可。最後再修改一下登入頁面 login.html 及 認證後的首頁 auth-home.html 。

 

  • 登入頁面 ( login.html )中的 Form
<form action="/login" method="post">        
<div class="input-group mb-3">          
  <input type="text" class="form-control" name="username" placeholder="Username">
  <div class="input-group-append">
 <div class="input-group-text">
   <span class="fas fa-envelope"></span>
 </div>
  </div>
</div>
<div class="input-group mb-3">
  <input type="password" class="form-control" name="password" placeholder="Password">
  <div class="input-group-append">
 <div class="input-group-text">
   <span class="fas fa-lock"></span>
 </div>
  </div>
</div>
<div class="row">
  <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
  <div class="col-8">
 <div class="icheck-primary">
   <input type="checkbox" id="remember">
   <label for="remember">
  Remember Me
   </label>
 </div>
  </div>
  <!-- /.col -->
  <div class="col-4">
 <button type="submit" class="btn btn-primary btn-block">Sign In</button>
  </div>
  <!-- /.col -->
</div>
</form>

 

  • 認證後的首頁 auth-home.html,加入登出的按鈕
<!-- Default box -->
<div class="card">
  <div class="card-header">
 <h3 class="card-title">Hi!! <#if username??>${username}</#if> 歡迎認證成功</h3>

 <div class="card-tools">
   <button type="button" class="btn btn-tool" data-card-widget="collapse" data-toggle="tooltip" title="Collapse">
  <i class="fas fa-minus"></i></button>
   <button type="button" class="btn btn-tool" data-card-widget="remove" data-toggle="tooltip" title="Remove">
  <i class="fas fa-times"></i></button>
 </div>
  </div>
  <div class="card-body">
 Your Role: 
 <#list roles as role>    
   ${role}   
 </#list>
 <form action="/logout" method="post">
   <input type="hidden"
    name="${_csrf.parameterName}"
    value="${_csrf.token}"/>
  <input type="submit" value="Logout">
 </form>

  </div>
  <!-- /.card-body -->
  <div class="card-footer">
 Footer
  </div>
  <!-- /.card-footer-->
</div>
<!-- /.card -->

 

這樣就可以執行了。在執行前,再來看一下整個專案的目錄結構吧!!

spring-securing-web-structure

 

Copyright © All rights reserved. | Newsphere by AF themes.