1. 程式人生 > >學習筆記——Spring Boot(9)

學習筆記——Spring Boot(9)

Spring Security許可權管理

  學習spring boot學深以後自然要接觸spring security許可權管理,所謂的spring security,就是我們平時接觸到的登入時面臨的多使用者多賬戶登入,還有使用者登入時的安全問題和許可權劃分的功能。可以說,spring security在進行登入頁設計的時候,提供了很多方便,而且攔截器的功能也包括在裡面,直接整合就可以了,對登入頁面設計也十分友好。

  首先當然是匯入spring security的依賴:

<!-- Spring Security 依賴 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

  當然,看過我部落格的同學也會發現我很喜歡使用thymeleaf作為前端的模板,而thymeleaf為了與spring security結合,也要匯入一個依賴(是為了前端設計才匯入的):

<!--內含thymeleaf與security結合的模板支援-->
<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity4</artifactId>
    <version>3.0.2.RELEASE</version>
</dependency>

而在需要spring security安全控制的前端html頁面中要加入:

<html xmlns="http://www.w3.org/1999/xhtml"
     xmlns:th="http://www.thymeleaf.org"
     xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">

僅第三條xmlns是新新增的。

  第一步由於我們是使用資料庫進行儲存我們使用者的賬號密碼的,所以我們需要建立資料庫連線,至於操作資料庫可以參考我另一篇部落格,我們第一步自然是要建立實體類user(使用者),而spring security要求除了實體user以為,我們還需要建立一個實體authority(許可權),用於關聯每個賬戶的許可權,除此之外,還有其他很多配置要做,我們先來建立第一個實體類user:

/* *

* 登陸者實體類
* 內含不同許可權不同管理許可權管理
* */

@Entity
@Table(name = "user")
//繼承UserDetails實現spring security的快取機制
public class User implements UserDetails ,Serializable {

    //user的序列化id
    private static final long serialVersionUID = 1L;

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

    @NotBlank
    @Column(nullable = false,length = 50)
    private String username;


    @NotBlank
    @Column(nullable = false,length = 50)
    private String password;


    //建立一個與Authority資料表對應的List,其中是多對多的關係
    //其中蘊含了一箇中間表user_authority說明兩個表的關係
    @ManyToMany(cascade = CascadeType.DETACH,fetch = FetchType.EAGER)
    @JoinTable(name = "user_authority",joinColumns  = @JoinColumn(name = "user_id",referencedColumnName = "id"),inverseJoinColumns = @JoinColumn(name = "authority_id",referencedColumnName = "id"))
    private List<Authority> authorities;


    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }


    @Override
    public String getPassword() {
        return password;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    @Override
    public String getUsername() {
        return username;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public static long getSerialVersionUID() {
        return serialVersionUID;
    }

    public void setAuthorities(List<Authority> authorities) {
        this.authorities = authorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        //需要將list<authority>轉換為List<SimpleGrantedAuthority>,否則會拿不到角色列表
        List<SimpleGrantedAuthority> simpleGrantedAuthorities=new ArrayList<>();
        for(GrantedAuthority authority:this.authorities){
            simpleGrantedAuthorities.add(new SimpleGrantedAuthority(authority.getAuthority()));
        }
        return simpleGrantedAuthorities;
    }


    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

記住觀察好我這個實體,其中spring security是要求繼承userDetails的,同時也要實現userDetails的各個方法,這是為了更好實現spring security的快取機制。而Serializable是用於序列化的,這也是必須的。另外,該實體類是與下面的authority許可權實體類建立了manytomany的資料庫關係的,是為了更好的管理賬戶的許可權。

下面是建立authority許可權的實體類:

/* *
* 許可權類,是與user進行耦合
* */
@Entity
@Table(name = "authority")
public class Authority implements GrantedAuthority {

    //authority的序列化id
    private static final long serialVersionUID = 1L;

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

    @Column(nullable = false)
    private String name;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    @Override
    public String getAuthority() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

這是許可權實體類,十分簡單,僅需繼承GrantedAuthority,且實現兩個引數id和name。

下面就是建立與資料庫的基礎連線了,也就是如果要對各個賬戶進行增刪改查,則需要建立repository和service進行管理。第一步是要建立user的repository:

public interface UserRepository extends JpaRepository<User,Integer> {

    //根據使用者名稱查詢使用者
    User findByUsername(String username);

}

十分簡單,但是記住一定要實現一個根據使用者名稱查詢使用者的方法,這在建立service中userDetailsService中要使用的。

而建立authority的repository更簡單:

public interface AuthorityRepository extends JpaRepository<Authority,Integer> {

}

下面就是建立service層了,首先是service層的介面,然後才是介面的實現類,兩個介面我就簡單發一下:

/* *
* user的service層介面
* */
public interface UserService  {

    List<User> findAll();

    User findById(Integer id);

    User saveOrUpdateUser(User user);

    void deleteById(Integer id);
}
/* *
* authority的service介面
* */
public interface AuthorityService {

    //根據id查詢許可權
    Authority getAuthorityById(Integer id);

    //查詢所有許可權
    List<Authority> findAll();
}

  下面是user類介面的實現類:

@Service
public class UserServiceImpl implements UserService, UserDetailsService {
    @Autowired
    private UserRepository userRepository;

    @Override
    @Transactional
    public List<User> findAll() {
        return userRepository.findAll();
    }

    @Override
    @Transactional
    public User findById(Integer id) {
        return userRepository.findOne(id);
    }


    @Override
    @Transactional
    public User saveOrUpdateUser(User user) {
        try{
           userRepository.save(user);
        }catch (Exception e){
            throw new RuntimeException("Add User Error: "+e.getMessage());
        }
        return user;
    }

    @Override
    @Transactional
    public void deleteById(Integer id) {
         userRepository.delete(id);
    }

    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(s);
        if(user==null){
            throw new UsernameNotFoundException("使用者名稱不存在");
        }//使用者不存在要丟擲異常
        return user;
    }
}

spring security要求繼承userDetailsService,然後要實現一個方法,就是loadUserByUsername,具體實現可以觀察上面。

  Authority許可權介面實現類實現十分簡單:

@Service
public class AuthorityServiceImpl implements AuthorityService {

    @Autowired
    private AuthorityRepository authorityRepository;

    @Override
    public Authority getAuthorityById(Integer id) {
        return authorityRepository.findOne(id);
    }

    @Override
    public List<Authority> findAll() {
        return authorityRepository.findAll();
    }
}

  現在我們對賬號管理就差不多做完了(除了controller,由於controller和我之前管理資料庫的部落格的實現差不多,也就不再展示了),對其的增刪改查就需要我們設計前端頁面進行。但是下面才是重點,也就是設計登入頁時的各類功能,也就是spring security的核心。首先我們需要建立spring security的配置類,通過配置類進行一系列操作:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)  //啟用安全認證
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    //定義一個key
    private static final String KEY = "scnu";

    //注入UserDetailsService
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();  //使用BCrypt加密
    }

    //實現方法authenticationProvider(),內含密碼加密
    @Bean
    public AuthenticationProvider authenticationProvider(){
        //DaoAuthenticationProvider用於從UserDetailsService中取出認證資訊
        DaoAuthenticationProvider daoAuthenticationProvider=new DaoAuthenticationProvider();
        daoAuthenticationProvider.setUserDetailsService(userDetailsService);
        daoAuthenticationProvider.setPasswordEncoder(passwordEncoder); //密碼加密
        return daoAuthenticationProvider;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/asserts/**","/login.html").permitAll()  //靜態資源可以訪問
                .antMatchers("/h2-console/**").permitAll() // h2控制檯都可以訪問
                .antMatchers("/admin/**").hasRole("ADMIN")  //管理頁需要admin角色才可以訪問
                .antMatchers("/setter/**").hasRole("USER")
                .and()
                .formLogin()  //基於form表單的訪問形式
                .loginPage("/login").defaultSuccessUrl("/dispath").failureUrl("/login-error")  //設定登入頁,成功後訪問的頁面和訪問錯誤頁
                .and().rememberMe().key(KEY)  //remember-me的設定
                .and().exceptionHandling().accessDeniedPage("/403");  //賬號密碼錯誤進入403介面
        http.csrf().ignoringAntMatchers("/h2-console/**"); // 禁用 H2 控制檯的 CSRF 防護
        http.headers().frameOptions().sameOrigin(); // 允許來自同一來源的H2 控制檯的請求
    }

    /* *
    * 認證資訊從資料庫從獲取
    * */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService);   //使用資料庫儲存的資訊
        auth.authenticationProvider(authenticationProvider());   //密碼加密使用BCrypt加密演算法
    }
}

我們需要將該配置類繼承WebSecurityConfigurerAdapter,使用註解@EnableWebSecurity宣告。該類主要改造了繼承的WebSecurityConfigurerAdapter中方法configure(HttpSecurity http)和configure(AuthenticationManagerBuilder auth),第一個方法是實現一些基礎的配置,比如哪些頁面可以訪問,哪些頁面需要什麼許可權才可以訪問,成功登陸會訪問什麼,密碼錯誤會訪問什麼,remember me等配置,我在該方法中都有註釋,可以自行觀察,另外,在remember me中需要一個KEY,我們需要在方法外宣告,至於KEY的值我們可以自行賦值。第二個方法是配置賬戶資訊通過資料庫儲存,同時密碼加密的形式,我密碼加密的形式是使用BCrypt加密演算法,也就是說上面有多個bean配置都是為了密碼加密而宣告的。

  現在來說一下與登入頁相關的controller層的設計,控制層的設計與剛剛配置類中的方法configure(HttpSecurity http)息息相關,我們觀察一下剛剛的方法:formLogin()表明是基於form表單的形式進行登入,loginPage(“/login”)指明登入頁的url,defaultSuccessful(“/dispath”)指明登入成功訪問的url,failureUrl(“/login-error”)指明當賬號密碼出現錯誤時訪問的url,我們可以根據這些url設計controller:

/* *
* 登入頁的控制層設定
* */
@Controller
public class LoginController {

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

    //發生賬號密碼錯誤時候
    @GetMapping("/login-error")
    public String errorMsg(Model model){
        model.addAttribute("loginError",true);
        model.addAttribute("errorMsg","賬號密碼錯誤!");
        return "login";
    }

    /* *
     * 該方法是根據不同使用者許可權跳轉至不同使用者所需介面
     * admin和user兩種使用者為主
     * */
    @GetMapping("/dispath")
    public String dispath(){
        //獲取當前使用者的許可權
        Set<String> roles = AuthorityUtils.authorityListToSet(SecurityContextHolder.getContext()
                .getAuthentication().getAuthorities());

        //設定變數儲存路徑地址
        String s="";
        if(roles.contains("ROLE_ADMIN")){
            s="redirect:/admin";
        }else if(roles.contains("ROLE_USER")){
            s="forward:/setter ";
        }        //根據許可權跳轉
        return s;
    }
}

可以觀察方法dispath()中,我們是根據使用者的不同許可權進行跳轉的。

  順便給一下登入頁前端頁面的form表單(基於thymeleaf):

<form class="form-signin" action="#" th:action="@{login}" method="post">
   <h1 class="h3 mb-3 font-weight-normal" >請登入</h1>
   <!--加入p標籤,標籤的顏色設定為紅色,標籤的內容由controller中msg獲得-->
   <!--使用if方法,同時變量表達式中的內建工具判斷msg是否為空-->
   <p style="color: red" th:text="${errorMsg}" th:if="${loginError}==true"></p>
   <label class="sr-only" >使用者名稱</label>
   <input type="text" name="username" class="form-control" placeholder="請輸入賬號"  required="" autofocus="">
   <label class="sr-only">密碼</label>
   <input type="password" name="password" class="form-control" placeholder="請輸入密碼"  required="">
   <div class="checkbox mb-3">
      <label>
        <input type="checkbox"  name="remember-me" >記住我
      </label>
   </div>
   <button class="btn btn-lg btn-primary btn-block" type="submit" >登入</button>
</form>

由於在配置中我們已經聲明瞭是基於form表單的形式,所以我們僅賦值name與相關的引數即可自動配置完。其中,remember me的功能是在之前配置類中方法configure(HttpSecurity http)中宣告rememberMe().key(KEY)中匯入,我們只需要在登入頁form表單中功能記住我宣告name為remember-me即可。

重點關注:

由於加入了spring security,所以在進行某些增刪改查的時候你會發現之前的方法不行,這是因為加入了跨域防護這種煩人的東西,但是沒有又不安全,所以我們需要在前端頁面中加入一些引數來增加跨域防護,但其實我自己學得也不太好,所以說得也不太好,首先我們需要在head標籤中加入:

<!-- CSRF -->
<meta name="_csrf" th:content="${_csrf.token}"/>
<!-- default header name is X-CSRF-TOKEN -->
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>

這是與csrf跨域防護的宣告,然後我們在設計按鈕(button或者a標籤)的時候,要使用JavaScript時進行按鈕設計時,在ajax的設計前我們需要引入:

<!--// 獲取 CSRF Token-->
var csrfToken = $("meta[name='_csrf']").attr("content");
var csrfHeader = $("meta[name='_csrf_header']").attr("content");

給個例子,我們首先聲明瞭一個刪除按鈕:

<!--刪除按鈕-->
<a class="btn btn-danger btn-sm deletebtn" role="button" data-th-attr="userId=${user.id}">刪除</a>

然後進行js(含ajax)設計:

<script>
          $(".deletebtn").click(function () {
              // 獲取 CSRF Token
              var csrfToken = $("meta[name='_csrf']").attr("content");
              var csrfHeader = $("meta[name='_csrf_header']").attr("content");
              if(window.confirm("你確定要刪除嗎?")) {
                  $.ajax({
                      url: "/admin/" + $(this).attr("userId"),
                      type: 'DELETE',
                      beforeSend: function (request) {
                          request.setRequestHeader(csrfHeader, csrfToken); // 新增  CSRF Token
                      },
                      success: function (data) {
                          alert("刪除成功!");
                          //成功了重新整理介面
                          $("#mainContainer").html(data);
                      }
                   })
                  return true;
              }
          })
</script>

大概就是這樣,但是裡面還有一些問題,由於我對前端知識的缺漏,所以會有一些問題,希望有大神可以指點一下。

另外,如果簡單的宣告一下為form表單的形式,前端會自動注入跨域防護,比如刪除按鈕這樣子宣告的話:

<!--刪除按鈕-->
<form  th:action="@{/admin/}+${user.id}" method="post">
   <input type="hidden" name="_method" value="delete">
   <!--刪除按鈕-->
   <button  type="submit" class="btn btn-danger btn-sm deletebtn">刪除</button>
</form>

它將會自動生成csrf防護(我也不清楚什麼機制)。