學習筆記——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防護(我也不清楚什麼機制)。