一 pom
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.fkit</groupId>
<artifactId>securitymybatistest</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>securitymybatistest</name>
<url>http://maven.apache.org</url>
<!-- spring-boot-starter-parent是Spring Boot的核心啓動器, 包含了自動配置、日誌和YAML等大量默認的配置,大大簡化了我們的開發。
引入之後相關的starter引入就不需要添加version配置, spring boot會自動選擇最合適的版本進行添加。 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.0.RELEASE</version>
<relativePath />
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- 添加spring-boot-starter-web模塊依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 添加spring-boot-starter-thymeleaf模塊依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 添加spring-boot-starter-security 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- 添加mysql數據庫驅動 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- 添加MyBatis依賴 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
二 啓動類
package org.fkit.securitymybatistest;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
// @SpringBootApplication指定這是一個 spring boot的應用程序.
@SpringBootApplication
// 掃描數據訪問層接口的包名。
@MapperScan("org.fkit.securitymybatistest.mapper")
public class App {
public static void main(String[] args) {
// SpringApplication 用於從main方法啓動Spring應用的類。
SpringApplication.run(App.class, args);
}
}
三 控制器
package org.fkit.securitytest.controller;
import java.util.ArrayList;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class AppController {
@RequestMapping("/")
public String index() {
return "index";
}
@RequestMapping(value = "/login")
public String login() {
return "login";
}
@RequestMapping("/home")
public String homePage(Model model) {
model.addAttribute("user", getUsername());
model.addAttribute("role", getAuthority());
return "home";
}
@RequestMapping(value = "/admin")
public String adminPage(Model model) {
model.addAttribute("user", getUsername());
model.addAttribute("role", getAuthority());
return "admin";
}
@RequestMapping(value = "/dba")
public String dbaPage(Model model) {
model.addAttribute("user", getUsername());
model.addAttribute("role", getAuthority());
return "dba";
}
@RequestMapping(value = "/accessDenied")
public String accessDeniedPage(Model model) {
model.addAttribute("user", getUsername());
model.addAttribute("role", getAuthority());
return "accessDenied";
}
@RequestMapping(value = "/logout")
public String logoutPage(HttpServletRequest request, HttpServletResponse response) {
// Authentication是一個接口,表示用戶認證信息
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
// 如果用戶認知信息不爲空,註銷
if (auth != null) {
new SecurityContextLogoutHandler().logout(request, response, auth);
}
// 重定向到login頁面
return "redirect:/login?logout";
}
/**
* 獲得當前用戶名稱
*/
private String getUsername() {
// 從SecurityContex中獲得Authentication對象代表當前用戶的信息
String username = SecurityContextHolder.getContext().getAuthentication().getName();
System.out.println("username = " + username);
return username;
}
/**
* 獲得當前用戶權限
*/
private String getAuthority() {
// 獲得Authentication對象,表示用戶認證信息。
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
List<String> roles = new ArrayList<String>();
// 將角色名稱添加到List集合
for (GrantedAuthority a : authentication.getAuthorities()) {
roles.add(a.getAuthority());
}
System.out.println("role = " + roles);
return roles.toString();
}
}
四 數據訪問接口
package org.fkit.securitymybatistest.mapper;
import java.util.List;
import org.apache.ibatis.annotations.Many;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.mapping.FetchType;
import org.fkit.securitymybatistest.pojo.FKRole;
import org.fkit.securitymybatistest.pojo.FKUser;
public interface UserMapper {
// 根據loginName查詢用戶信息,同時關聯查詢出用戶的權限
@Select("select * from tb_user where login_name = #{loginName}")
@Results({
@Result(id=true,column="id",property="id"),
@Result(column="login_name",property="loginName"),
@Result(column="password",property="password"),
@Result(column="username",property="username"),
@Result(column="id",property="roles",
many=@Many(select="findRoleByUser",
fetchType=FetchType.EAGER))
})
FKUser findByLoginName(String loginName);
// 根據用戶id關聯查詢用戶的所有權限
@Select(" SELECT id,authority FROM tb_role r,tb_user_role ur "
+ " WHERE r.id = ur.role_id AND user_id = #{id}")
List<FKRole> findRoleByUser(Long id);
}
五 創建自定義服務類
package org.fkit.securitymybatistest.service;
import java.util.ArrayList;
import java.util.List;
import org.fkit.securitymybatistest.mapper.UserMapper;
import org.fkit.securitymybatistest.pojo.FKRole;
import org.fkit.securitymybatistest.pojo.FKUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
/**
* 需要實現UserDetailsService接口
* 因爲在Spring Security中我們配置相關參數需要UserDetailsService類型的數據
* */
@Service
public class UserService implements UserDetailsService{
// 注入持久層接口UserMapper
@Autowired
UserMapper userMapper;
// 實現接口中的loadUserByUsername方法,通過該方法查詢到對應的用戶
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 調用持久層接口findByLoginName方法查找用戶,此處的傳進來的參數實際是loginName
FKUser fkUser = userMapper.findByLoginName(username);
// System.out.println("user = " + fkUser);
if (fkUser == null) {
throw new UsernameNotFoundException("用戶名不存在");
}
// 創建List集合,用來保存用戶權限,GrantedAuthority對象代表賦予給當前用戶的權限
List<GrantedAuthority> authorities = new ArrayList<>();
// 獲得當前用戶權限集合
List<FKRole> roles = fkUser.getRoles();
for (FKRole role : roles) {
// 將關聯對象Role的authority屬性保存爲用戶的認證權限
authorities.add(new SimpleGrantedAuthority(role.getAuthority()));
}
// 此處返回的是org.springframework.security.core.userdetails.User類,該類是Spring Security內部的實現
return new User(fkUser.getUsername(), fkUser.getPassword(), authorities);
}
}
六 認證處理類
1 AppAuthenticationSuccessHandler
package org.fkit.securitytest.security;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.DefaultRedirectStrategy;
import org.springframework.security.web.RedirectStrategy;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
@Component
public class AppAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
// Spring Security 通過RedirectStrategy對象負責所有重定向事務
private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
/*
* 重寫handle方法,方法中通過RedirectStrategy對象重定向到指定的url
*/
@Override
protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
throws IOException {
// 通過determineTargetUrl方法返回需要跳轉的url
String targetUrl = determineTargetUrl(authentication);
// 重定向請求到指定的url
redirectStrategy.sendRedirect(request, response, targetUrl);
}
/*
* 從Authentication對象中提取角色提取當前登錄用戶的角色,並根據其角色返回適當的URL。
*/
protected String determineTargetUrl(Authentication authentication) {
String url = "";
// 獲取當前登錄用戶的角色權限集合
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
List<String> roles = new ArrayList<String>();
// 將角色名稱添加到List集合
for (GrantedAuthority a : authorities) {
roles.add(a.getAuthority());
}
// 判斷不同角色跳轉到不同的url
if (isAdmin(roles)) {
url = "/admin";
} else if (isUser(roles)) {
url = "/home";
} else {
url = "/accessDenied";
}
System.out.println("url = " + url);
return url;
}
private boolean isUser(List<String> roles) {
if (roles.contains("ROLE_USER")) {
return true;
}
return false;
}
private boolean isAdmin(List<String> roles) {
if (roles.contains("ROLE_ADMIN")) {
return true;
}
return false;
}
public void setRedirectStrategy(RedirectStrategy redirectStrategy) {
this.redirectStrategy = redirectStrategy;
}
protected RedirectStrategy getRedirectStrategy() {
return redirectStrategy;
}
}
2 AppSecurityConfigurer
package org.fkit.securityjpatest.security;
import org.fkit.securityjpatest.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* 自定義Spring Security認證處理類的時候 我們需要繼承自WebSecurityConfigurerAdapter來完成,相關配置重寫對應
* 方法即可。
*/
@Configuration
public class AppSecurityConfigurer extends WebSecurityConfigurerAdapter {
// 依賴注入用戶服務類
@Autowired
private UserService userService;
// 依賴注入加密接口
@Autowired
private PasswordEncoder passwordEncoder;
// 依賴注入用戶認證接口
@Autowired
private AuthenticationProvider authenticationProvider;
// 依賴注入認證處理成功類,驗證用戶成功後處理不同用戶跳轉到不同的頁面
@Autowired
AppAuthenticationSuccessHandler appAuthenticationSuccessHandler;
/*
* BCryptPasswordEncoder是Spring Security提供的PasswordEncoder接口是實現類
* 用來創建密碼的加密程序,避免明文存儲密碼到數據庫
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// DaoAuthenticationProvider是Spring Security提供AuthenticationProvider的實現
@Bean
public AuthenticationProvider authenticationProvider() {
// 創建DaoAuthenticationProvider對象
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
// 不要隱藏"用戶未找到"的異常
provider.setHideUserNotFoundExceptions(false);
// 通過重寫configure方法添加自定義的認證方式。
provider.setUserDetailsService(userService);
// 設置密碼加密程序認證
provider.setPasswordEncoder(passwordEncoder);
return provider;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
System.out.println("AppSecurityConfigurer configure auth......");
// 設置認證方式。
auth.authenticationProvider(authenticationProvider);
}
/**
* 設置了登錄頁面,而且登錄頁面任何人都可以訪問,然後設置了登錄失敗地址,也設置了註銷請求,註銷請求也是任何人都可以訪問的。
* permitAll表示該請求任何人都可以訪問,.anyRequest().authenticated(),表示其他的請求都必須要有權限認證。
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("AppSecurityConfigurer configure http......");
http.authorizeRequests()
// spring-security 5.0 之後需要過濾靜態資源
.antMatchers("/login", "/css/**", "/js/**", "/img/*").permitAll().antMatchers("/", "/home")
.hasRole("USER").antMatchers("/admin/**").hasAnyRole("ADMIN", "DBA").anyRequest().authenticated().and()
.formLogin().loginPage("/login").successHandler(appAuthenticationSuccessHandler)
.usernameParameter("loginName").passwordParameter("password").and().logout().permitAll().and()
.exceptionHandling().accessDeniedPage("/accessDenied");
}
}
七 視圖
1 login.html
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4">
<head>
<title>Spring Boot Security示例</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
<link rel="stylesheet" th:href="@{css/app.css}" />
<link rel="stylesheet" th:href="@{css/bootstrap-theme.min.css}" />
<link rel="stylesheet" type="text/css"
href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.2.0/css/font-awesome.css" />
<script type="text/javascript" th:src="@{js/jquery-1.11.0.min.js}"></script>
<script type="text/javascript" th:src="@{js/bootstrap.min.js}"></script>
<script type="text/javascript">
$(function() {
$("#loginBtn").click(function() {
var loginName = $("#loginName");
var password = $("#password");
var msg = "";
if (loginName.val() == "") {
msg = "登錄名稱不能爲空!";
loginName.focus();
} else if (password.val() == "") {
msg = "密碼不能爲空!";
password.focus();
}
if (msg != "") {
alert(msg);
return false;
}
$("#loginForm").submit();
});
});
</script>
</head>
<body>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">簡單Spring Boot Security示例</h3>
</div>
</div>
<div id="mainWrapper">
<div class="login-container">
<div class="login-card">
<div class="login-form">
<!-- 表單提交到login -->
<form id="loginForm" th:action="@{/login}" method="post"
class="form-horizontal">
<!-- 用戶名或密碼錯誤提示 -->
<div th:if="${param.error != null}">
<div class="alert alert-danger">
<p>
<font color="red">用戶名或密碼錯誤!</font>
</p>
</div>
</div>
<!-- 註銷提示 -->
<div th:if="${param.logout != null}">
<div class="alert alert-success">
<p>
<font color="red">用戶已註銷成功!</font>
</p>
</div>
</div>
<div class="input-group input-sm">
<label class="input-group-addon"><i class="fa fa-user"></i></label>
<input type="text" class="form-control" id="loginName"
name="loginName" placeholder="請輸入用戶名" />
</div>
<div class="input-group input-sm">
<label class="input-group-addon"><i class="fa fa-lock"></i></label>
<input type="password" class="form-control" id="password"
name="password" placeholder="請輸入密碼" />
</div>
<div class="form-actions">
<input id="loginBtn" type="button"
class="btn btn-block btn-primary btn-default" value="登錄" />
</div>
</form>
</div>
</div>
</div>
</div>
</body>
</html>
2 home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"></meta>
<title>home頁面</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
<link rel="stylesheet" th:href="@{css/bootstrap-theme.min.css}" />
<script type="text/javascript" th:src="@{js/jquery-1.11.0.min.js}"></script>
<script type="text/javascript" th:src="@{js/bootstrap.min.js}"></script>
</head>
<body>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">Home頁面</h3>
</div>
</div>
<h3>
歡迎[<font color="red"><span th:text="${user}">用戶名</span></font>]訪問Home頁面!
您的權限是<font color="red"><span th:text="${role}">權限</span></font><br />
<br /> <a href="admin">訪問admin頁面</a><br />
<br /> <a href="logout">安全退出</a>
</h3>
</body>
</html>
3 admin.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"></meta>
<title>admin頁面</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
<link rel="stylesheet" th:href="@{css/bootstrap-theme.min.css}" />
<script type="text/javascript" th:src="@{js/jquery-1.11.0.min.js}"></script>
<script type="text/javascript" th:src="@{js/bootstrap.min.js}"></script>
</head>
<body>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">Admin頁面</h3>
</div>
</div>
<h3>
歡迎[<font color="red"><span th:text="${user}">用戶名</span></font>]訪問Admin頁面!
您的權限是<font color="red"><span th:text="${role}">權限</span></font><br />
<br /> <a href="dba">訪問dba頁面</a><br />
<br /> <a href="logout">安全退出</a>
</h3>
</body>
</html>
4 dba.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"></meta>
<title>dba頁面</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
<link rel="stylesheet" th:href="@{css/bootstrap-theme.min.css}" />
<script type="text/javascript" th:src="@{js/jquery-1.11.0.min.js}"></script>
<script type="text/javascript" th:src="@{js/bootstrap.min.js}"></script>
</head>
<body>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">DBA頁面</h3>
</div>
</div>
<h3>
歡迎[<font color="red"><span th:text="${user}">用戶名</span></font>]訪問訪問DBA頁面!
您的權限是<font color="red"><span th:text="${role}">權限</span></font><br />
<br /> <a href="logout">安全退出</a>
</h3>
</body>
</html>
5 accessDenied.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8"></meta>
<title>訪問拒絕頁面</title>
<link rel="stylesheet" th:href="@{css/bootstrap.min.css}" />
<link rel="stylesheet" th:href="@{css/bootstrap-theme.min.css}" />
<script type="text/javascript" th:src="@{js/jquery-1.11.0.min.js}"></script>
<script type="text/javascript" th:src="@{js/bootstrap.min.js}"></script>
</head>
<body>
<div class="panel panel-primary">
<div class="panel-heading">
<h3 class="panel-title">AccessDenied頁面</h3>
</div>
</div>
<h3>
<font color="red"><span th:text="${user}">用戶名</span></font>,
您沒有權限訪問頁面! 您的權限是<font color="red"><span th:text="${role}">權限</span></font><br />
<br /> <a href="logout">安全退出</a>
</h3>
</body>
</html>
八 創建持久化類
1 FKRole
package org.fkit.securitymybatistest.pojo;
import java.io.Serializable;
public class FKRole implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String authority;
public FKRole() {
super();
// TODO Auto-generated constructor stub
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getAuthority() {
return authority;
}
public void setAuthority(String authority) {
this.authority = authority;
}
@Override
public String toString() {
return "FKRole [id=" + id + ", authority=" + authority + "]";
}
}
2 FKUser
package org.fkit.securitymybatistest.pojo;
import java.io.Serializable;
import java.util.List;
public class FKUser implements Serializable{
private static final long serialVersionUID = 1L;
private Long id;
private String loginName;
private String username;
private String password;
private List<FKRole> roles;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getLoginName() {
return loginName;
}
public void setLoginName(String loginName) {
this.loginName = loginName;
}
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 List<FKRole> getRoles() {
return roles;
}
public void setRoles(List<FKRole> roles) {
this.roles = roles;
}
@Override
public String toString() {
return "FKUser [id=" + id + ", loginName=" + loginName + ", username=" + username + ", password=" + password
+ ", roles=" + roles + "]";
}
}
九 創建數據庫Springboot
相關數據表如下:
十 配置文件
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springboot
spring.datasource.username=root
spring.datasource.password=
logging.level.org.springframework.security=info
logging.level.org.fkit.securitymybatistest.mapper.UserMapper=debug
spring.thymeleaf.cache=false
十一 測試