版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
一 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
十一 测试