1. 程式人生 > >Spring學習之旅(八) Spring Security的使用

Spring學習之旅(八) Spring Security的使用

辛苦堆砌,轉載請註明出處,謝謝!

        之前的User校驗我們自己通過比較使用者名稱和密碼來完成,這樣可能存在一些安全隱患,還需要自己處理Session的問題。本篇文章使用Spring Security進行安全校驗,對專案進行重構。

        Spring Security是Spring實現的安全框架,可以對請求和方法進行安全保護,Spring Security根本上是一套Filter鏈,當配置使用Spring Security時,Spring會向專案中新增Filter,從而對請求進行攔截,並進行必要的安全校驗。

        首先,為了讓Spring支援Spring Security,需要新增必要的依賴,這是因為Spring Security不屬於Spring Framework,是一個獨立的專案。

<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-web</artifactId>
	<version>${spring-security-web.version}</version>
</dependency>
<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-config</artifactId>
	<version>${spring-security-web.version}</version>
</dependency>
然後,建立如下的類
package com.yjp.springmvc.blog.config;

import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;

public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
}
該類派生自AbstractSecurityWebApplicationInitializer,這樣,當啟用Spring Security時,會加入安全校驗需要的Filter鏈。最後,啟用Spring Security,並配置攔截規則和使用者資料源
package com.yjp.springmvc.blog.config;

import org.springframework.beans.factory.annotation.Autowired;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	
	@Autowired
	private UserDetailsService userDetailsService;
	
	@Override
	protected void configure(HttpSecurity http) throws Exception {
		//配置攔截規則
		http
			.formLogin()
				.loginPage("/login")
				.failureUrl("/loginError")
			.and()
			.logout()
				.logoutSuccessUrl("/login")
			.and()
			.authorizeRequests()
				.antMatchers("/").authenticated()
				.antMatchers("/**").permitAll();
	}
	
	@Override
	protected void configure(AuthenticationManagerBuilder auth) 
			throws Exception {
		auth.userDetailsService(userDetailsService);
	}
}
我們派生了WebSecurityConfigurerAdapter類,並重新實現了兩個方法,其中,configure以HttpSecurity為引數的方法,用來定製攔截規則,我們這裡攔截"/"請求,其他請求放行,然後通過表單認證,認證表單為login,驗證失敗進入loginError請求,登出時,預設會進入logout請求,登出成功跳轉到login請求。通過configure以AuthenticationManagerBuilder為引數的方法,我們返回資料來源,資料來源Spring會從UserDetailsService獲取。記得將該配置類引入到RootConfig中。

        下面調整我們之前的專案,刪除LoginController,建立SecurityController

package com.yjp.springmvc.blog.web.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class SecurityController {
	
	@RequestMapping(method=RequestMethod.GET, value="/login")
	public String login(String error) {
		return "login";
	}
	
	@RequestMapping(method=RequestMethod.GET, value="/loginError")
	public String loginError(Model model) {
		model.addAttribute("error", "使用者名稱或密碼錯誤");
		return "login";
	}
}
主要用來處理login流程的請求,將請求與對應的檢視掛鉤,看看login.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page isELIgnored="false"%>
<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>簡微</title>
	<link rel="stylesheet" type="text/css" href="resources/css/login.css">
	<link rel="stylesheet" type="text/css" href="resources/css/error.css">
</head>
<body>
	<div class="loginPanel">
	    <div>
	        <img src="resources/images/logo.png" alt="簡微"/>
	    </div>
	    <form method="post" action="login">
	        <table>
	            <tr>
	                <td colspan="2" align="center" style="font-weight:bold">會員登入</td>
	            </tr>
	            <tr>
	                <td>名稱:</td>
	                <td><input type="text" name="username"/></td>
	            </tr>
	            <tr>
	                <td>密碼:</td>
	                <td><input type="password" name="password"/></td>
	            </tr>
	            <tr>
	        		<td colspan="2" align="center"><span class="error">${error}</span></td>
	        	</tr>
	            <tr>
	                <td colspan="2" align="center"><input type="submit" value="登入"></td>
	            </tr>
            	<tr>
            		<td align="center"><a href="registerPage">註冊</a></td>
                	<td align="center"><a href="forgotPage">忘記密碼?</a></td>
            	</tr>
	        </table>
	        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
	    </form>
    </div>
    <div>
        <h1>簡微</h1>
        <ul>
	        <li>說你想說</li>
	        <li>看你想看</li>
	        <li>就這麼簡單</li>
    	</ul>
    </div>
</body>
</html>
刪除了之前用Spring標籤庫做的內容,表單傳送POST請求到login,這裡千萬注意,我們攔截規則的formPage中的/login是GET請求,會發送到我們的SecurityController處理,然後返回login.jsp檢視,而視圖表單中的login是POST給Spring Security處理的,會完成及鑑權相關的工作。另外,編單中添加了一個
<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
這個是防止CSRF攻擊的,Spring Security要求要有這個表單域。這樣,如果我們輸入的使用者名稱密碼與資料來源比較通過校驗,就會進入"/"請求,我們在HomeController中處理該請求
package com.yjp.springmvc.blog.web.controller;

import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
public class HomeController {
	@RequestMapping(method=RequestMethod.GET, value="/")
	public String home(Model model) {
		//通過Spring Security獲取當前的使用者
		UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
			    .getAuthentication()
			    .getPrincipal();
		
		model.addAttribute("username", userDetails.getUsername());
		return "home";
	}
}
該請求會跳轉到home.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ page isELIgnored="false"%>
<!DOCTYPE html">
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
	<title>簡微</title>
	<link rel="stylesheet" href="resources/css/home.css" type="text/css">
</head>
<body>
	<div class="leftPanel">
		<img src="resources/images/logo.png" alt="簡微" /><br><br>
		<form method="post" action="logout" style="">
			<input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>
			<button type="submit">登出 ${username}</button>
		</form>
	</div>
	
	<form method="post" action="message">
		說說你的事...<br><br>
        <textarea cols='60' rows='4' name='blabla'></textarea><br><br>
		<button type="submit">送出</button>
	</form>
</body>
</html>
這裡的home.jsp我們已經完成了登出功能,注意有一個傳送POST logout請求的表單,這個同樣的道理,登出流程會交給Spring Security處理,主要是清除及鑑權相關的資料。

        最後,由於我們要給鑑權提供資料來源,持久層做了相應的改動,首先看看UserService

package com.yjp.springmvc.blog.beans.service;

import org.springframework.security.core.userdetails.UserDetailsService;

import com.yjp.springmvc.blog.beans.model.User;

public interface UserService extends UserDetailsService {
	boolean saveUser(User user);
}
package com.yjp.springmvc.blog.beans.service;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.yjp.springmvc.blog.beans.model.User;
import com.yjp.springmvc.blog.beans.repository.UserRepository;

@Service
public class UserServiceImpl implements UserService {
	
	@Autowired
	private UserRepository userRepository;
	
	@Override
	public boolean saveUser(User user) {
		User saveUser = userRepository.save(user);
		return saveUser != null;
	}

	@Override
	public UserDetails loadUserByUsername(String username) 
			throws UsernameNotFoundException {
		UserDetails findUser = 
				userRepository.findUserByUsername(username);
		if (findUser != null) {
			return findUser;
		} else {
			return null;
		}
	}
}
UserService實現了UserDetailsService介面,用來提供資料來源,我們的資料來源就是User物件,但是User物件實現了UserDetails介面
package com.yjp.springmvc.blog.beans.model;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

import org.hibernate.annotations.GenericGenerator;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

@Entity
@Table(name="users")
public class User implements Serializable, UserDetails {
			
	private static final long serialVersionUID = 9038460243059691075L;
	
	@Id
	@GenericGenerator(strategy = "assigned", name = "username")
	private String username;
	
	@Column
	private String password;
	
	@Column
	private String email;
	
	public User() {}
	
	public User(String username, String password, String email) {
		this.username = username;
		this.password = password;
		this.email = email;
	}

	public String getUsername() {
		return username;
	}
	
	public void setUsername(String username) {
		this.username = username;
	}
	
	public String getPassword() {
		return password;
	}
	
	public void setPassword(String password) {
		this.password = password;
	}
	
	public String getEmail() {
		return email;
	}
	
	public void setEmail(String email) {
		this.email = email;
	}

	@Override
	public Collection<? extends GrantedAuthority> getAuthorities() {
		//返回使用者許可權
		return Arrays.asList(new SimpleGrantedAuthority("USER"));
	}

	@Override
	public boolean isAccountNonExpired() {
		//賬戶是否會過期
		return true;
	}

	@Override
	public boolean isAccountNonLocked() {
		//使用者是否被鎖定
		return true;
	}

	@Override
	public boolean isCredentialsNonExpired() {
		//密碼是否會過期
		return true;
	}

	@Override
	public boolean isEnabled() {
		//使用者是否使能
		return true;
	}

}
UserDetails介面的方法已經添加註釋,可以酌情修改。這樣就完成了Spring Security的配置,並將其使用在了我們的專案中。