SpringBoot整合SpringSecurity
源碼地址:https://gitee.com/lin8081/LWH10
1.基本介紹
SpringSecurity概念
- SpringSecurity是一個安全管理框架,提供了認證與授權這些基本操作
- 認證: 用戶訪問系統,系統校驗用戶身份是否合法的過程就是認證。常見的認證: 登陸認證。
- 授權:用戶認證後,訪問系統資源,校驗用戶是否有權限訪問系統資源的過程就是授權訪問校驗,簡稱爲授權。權限校驗過程:1.獲取用戶的權限; 2. 知道訪問資源需要的權限;3.拿着訪問資源需要的權限去用戶權限列表查找,找到則授權訪問。否則拒絕訪問。
2.創建數據庫表
一般權限控制有三層,即:用戶
<–>角色
<–>權限
,用戶與角色是多對多,角色和權限也是多對多。這裏我們先暫時不考慮權限,只考慮用戶
<–>角色
。 這裏爲了測試,表結構簡單設計,後續可以根據業務添加先關字段。
數據庫:Mysql 5.6 創建表結構:
-- 用戶表
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(255) NOT NULL COMMENT '賬號',
`password` varchar(255) NOT NULL COMMENT '密碼',
`true_name` varchar(50) DEFAULT NULL COMMENT '真實姓名',
`phone` varchar(50) DEFAULT NULL COMMENT '手機',
`email` varchar(25) DEFAULT NULL COMMENT '郵箱',
`createtime` varchar(25) DEFAULT NULL COMMENT '創建時間',
`status` int(2) DEFAULT NULL COMMENT '狀態',
`remarks` varchar(1000) DEFAULT NULL COMMENT '備註',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8
-- 角色表
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL,
`name` varchar(50) DEFAULT NULL COMMENT '角色名',
`roleDesc` varchar(50) DEFAULT NULL COMMENT '角色說明',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
-- 用戶-角色關係表
CREATE TABLE `sys_user_role` (
`user_id` int(11) NOT NULL,
`role_id` int(11) NOT NULL,
PRIMARY KEY (`user_id`,`role_id`),
KEY `fk_role_id` (`role_id`),
CONSTRAINT `fk_role_id` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_user_id` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8
初始化數據:
insert into `sys_role` values ('1', 'ROLE_ADMIN','管理員');
insert into `sys_role` values ('2', 'ROLE_USER','普通用戶');
insert into `sys_user` (id, username,password) values ('1', 'admin', '123');
insert into `sys_user` (id, username,password) values ('2', 'user', '123');
INSERT INTO `sys_user_role` VALUES ('1', '1');
INSERT INTO `sys_user_role` VALUES ('2', '2');
**注意:**這裏的權限格式爲ROLE_XXX
,是Spring Security規定的,不要亂起名字。
3.準備頁面
用於登錄的 login.html
片段以及用戶登錄成功後跳轉的 index.html
,將其放置在工程 resources/static
目錄下:
login.html片段:
<form method="post" action="/login">
<!-- 賬號 -->
<div class="layui-form-item">
<label class="layui-form-label">賬號</label>
<div class="layui-input-block">
<input name="username" id="userName" value="admin" placeholder="默認賬號:admin" type="text" lay-verify="required" class="layui-input">
</div>
</div>
<!-- 密碼 -->
<div class="layui-form-item">
<label class="layui-form-label">密碼</label>
<div class="layui-input-block">
<input name="password" id="password" value="123" placeholder="默認密碼:111111" type="password" lay-verify="required" class="layui-input">
</div>
</div>
<div>
<button type="submit" class="layui-btn layui-btn-fluid" >登 錄</button>
</div>
</form>
首頁index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>登陸成功</h1>
<a href="/admin">檢測ROLE_ADMIN角色</a>
<a href="/user">檢測ROLE_USER角色</a>
<button onclick="window.location.href='/logout'">退出登錄</button>
</body>
</html>
4.導入依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.5.RELEASE</version>
<relativePath/>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.34</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
</dependencies>
5.配置application.yml
server:
port: 9000
spring:
application:
name: auth01
datasource:
druid:
url: jdbc:mysql://localhost:3306/auth01?useUnicode=true&useJDBCCompliantTimezoneShift=true&serverTimezone=UTC&characterEncoding=utf8
username: root
password: root
driverClassName: com.mysql.jdbc.Driver
initialSize: 5 #初始建立連接數量
minIdle: 5 #最小連接數量
maxActive: 20 #最大連接數量
maxWait: 10000 #獲取連接最大等待時間,毫秒
testOnBorrow: true #申請連接時檢測連接是否有效
testOnReturn: false #歸還連接時檢測連接是否有效
timeBetweenEvictionRunsMillis: 60000 #配置間隔檢測連接是否有效的時間(單位是毫秒)
minEvictableIdleTimeMillis: 300000 #連接在連接池的最小生存時間(毫秒)
# mybatis
mybatis:
#config-location: classpath:mybatis/mybatis-config.xml # mybatis配置文件位置
mapper-locations:
- classpath*:mapper/**/*.xml # mapper映射文件位置
type-aliases-package: com.lwh.model # 別名包
configuration:
cache-enabled: true
6.創建實體類、DAO、Service和Controller
實體類:
-
SysUser:
public class SysUser implements Serializable{ private static final long serialVersionUID = -2836223054703407171L; private Integer id; private String username; private String password; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } 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; } }
-
SysRole:
public class SysRole implements Serializable { private static final long serialVersionUID = 7510551869226022669L; private Integer id; private String name; public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } }
-
SysUserRole:
public class SysUserRole implements Serializable{ private static final long serialVersionUID = -3256750757278740295L; private Integer userId; private Integer roleId; public SysUserRole() { } public SysUserRole(Integer userId, Integer roleId) { this.userId = userId; this.roleId = roleId; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } public Integer getRoleId() { return roleId; } public void setRoleId(Integer roleId) { this.roleId = roleId; } }
DAO:
-
SysUserMapper:
@Mapper public interface SysUserMapper { SysUser selectById(Integer id); SysUser selectByName(String username); }
-
SysRoleMapper:
@Mapper public interface SysRoleMapper { SysRole selectById(Integer id); }
-
SysUserRoleMapper:
@Mapper public interface SysUserRoleMapper { List<SysUserRole> listByUserId(Integer userId); }
Service:
-
SysUserService:
接口
public interface SysUserService { SysUser selectById(Integer id); SysUser selectByName(String name); }
實現類
@Service public class SysUserServiceImpl implements SysUserService { @Autowired private SysUserMapper userMapper; @Override public SysUser selectById(Integer id) { return userMapper.selectById(id); } @Override public SysUser selectByName(String name) { return userMapper.selectByName(name); } }
-
SysRoleService:
接口
public interface SysRoleService { public SysRole selectById(Integer id); }
實現類
@Service public class SysRoleServiceImpl implements SysRoleService { @Autowired private SysRoleMapper roleMapper; @Override public SysRole selectById(Integer id) { return roleMapper.selectById(id); } }
-
SysUserRoleService:
接口
public interface SysUserRoleService { List<SysUserRole> listByUserId(Integer userId); }
實現類
@Service public class SysUserRoleServiceImpl implements SysUserRoleService { @Autowired private SysUserRoleMapper userRoleMapper; @Override public List<SysUserRole> listByUserId(Integer userId) { return userRoleMapper.listByUserId(userId); } }
Controller:
@Controller
public class LoginController {
private Logger logger = LoggerFactory.getLogger(LoginController.class);
@GetMapping("/login")
public String showLogin() {
return "/pages/login.html";
}
@GetMapping("/")
public String showHome() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
logger.info("當前登陸用戶:" + username);
return "/pages/index.html";
}
@GetMapping("/admin")
@ResponseBody
@PreAuthorize("hasRole('ROLE_ADMIN')")
public String printAdmin() {
return "如果你看見這句話,說明你有ROLE_ADMIN角色";
}
@GetMapping("/user")
@ResponseBody
@PreAuthorize("hasRole('ROLE_USER')")
public String printUser() {
return "如果你看見這句話,說明你有ROLE_USER角色";
}
}
注意:如代碼所示:
- 獲取當前登錄用戶:
SecurityContextHolder.getContext().getAuthentication()
@PreAuthorize
用於判斷用戶是否有指定權限,沒有就不能訪問
7.配置mapper.xml
UserMapper.xml
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.lwh.dao.SysUserMapper" >
<resultMap id="BaseResultMap" type="com.lwh.model.user.SysUser" >
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="username" property="username" jdbcType="VARCHAR"/>
<result column="password" property="password" jdbcType="VARCHAR"/>
<result column="true_name" property="trueName" jdbcType="VARCHAR"/>
<result column="remarks" property="remarks" jdbcType="VARCHAR"/>
<result column="phone" property="phone" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List" >
id,username,password,true_name,remarks,phone
</sql>
<select id="selectById" resultMap="BaseResultMap" >
SELECT
<include refid="Base_Column_List" />
FROM sys_user WHERE id = #{id}
</select>
<select id="selectByName" resultMap="BaseResultMap" >
SELECT
<include refid="Base_Column_List" />
FROM sys_user WHERE username = #{username}
</select>
</mapper>
RoleMapper.xml
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.lwh.dao.SysRoleMapper" >
<resultMap id="BaseResultMap" type="com.lwh.model.user.SysRole" >
<id column="id" property="id" jdbcType="INTEGER"/>
<result column="name" property="name" jdbcType="VARCHAR"/>
</resultMap>
<sql id="Base_Column_List" >
id,name
</sql>
<select id="selectById" resultMap="BaseResultMap" >
SELECT
<include refid="Base_Column_List" />
FROM sys_role WHERE id = #{id}
</select>
</mapper>
UserRoleMapper.xml
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.lwh.dao.SysUserRoleMapper" >
<resultMap id="BaseResultMap" type="com.lwh.model.user.SysUserRole" >
<id column="user_id" property="userId" jdbcType="INTEGER"/>
<id column="role_id" property="roleId" jdbcType="INTEGER"/>
</resultMap>
<sql id="Base_Column_List" >
user_id,role_id
</sql>
<select id="listByUserId" resultMap="BaseResultMap" >
SELECT
<include refid="Base_Column_List" />
FROM sys_user_role WHERE user_id = #{userId}
</select>
</mapper>
8.配置SpringSecurity
1.UserDetailService
自定義 UserDetailService
, 將用戶信息和權限注入進來。
CustomUserDetailsService:
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private SysUserService userService;
@Autowired
private SysRoleService roleService;
@Autowired
private SysUserRoleService userRoleService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Collection<GrantedAuthority> authorities = new ArrayList<>();
// 從數據庫中取出用戶信息
SysUser user = userService.selectByName(username);
// 判斷用戶是否存在
if(user == null) {
throw new UsernameNotFoundException("用戶名不存在");
}
// 添加權限
List<SysUserRole> userRoles = userRoleService.listByUserId(user.getId());
for (SysUserRole userRole : userRoles) {
SysRole role = roleService.selectById(userRole.getRoleId());
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
// 返回UserDetails實現類
return new User(user.getUsername(), user.getPassword(), authorities);
}
}
2.WebSecurityConfig
Spring Security默認是禁用註解的,要想開啓註解, 需要在繼承WebSecurityConfigurerAdapter
的類上加@EnableGlobalMethodSecurity
註解, 來判斷用戶對某個控制層的方法是否具有訪問權限
例如:上面代碼方法前不加@preAuthorize註解,意味着所有用戶都能訪問方法,如果加上註解,表示只要具備指定角色的用戶纔有權限訪問。也就是得繼承WebSecurityConfigurerAdapter
類並加上@EnableGlobalMethodSecurity
註解才能在控制層加@preAuthorize註解
@EnableGlobalMethodSecurity詳解:
@EnableGlobalMethodSecurity(securedEnabled=true)
開啓@Secured
註解過濾權限@EnableGlobalMethodSecurity(jsr250Enabled=true)
開啓@RolesAllowed
註解過濾權限@EnableGlobalMethodSecurity(prePostEnabled=true)
使用表達式實現方法級別的安全性 ,4個註解可用:@PreAuthorize
在方法調用之前,基於表達式的計算結果來限制對方法的訪問@PostAuthorize
允許方法調用,但是如果表達式計算結果爲false,將拋出一個安全性異常@PostFilter
允許方法調用,但必須按照表達式來過濾方法的結果@PreFilter
允許方法調用,但必須在進入方法之前過濾輸入值
首先,我們將自定義的 userDetailsService
注入進來,在 configure()
方法中使用 auth.userDetailsService()
方法替換掉默認的 userDetailsService
。
這裏我們還指定了密碼的加密模式(5.0版本強制要求設置),我們採用SpringSecurity提供的加密模式:BCryptPasswordEncoder
,它幫我們實現了PasswordEncoder
,當然也可以自定義加密模式。
- WebSecurityConfig.java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 如果有允許匿名的url,填在下面
// .antMatchers().permitAll()
.anyRequest().authenticated()
.and()
// 設置登陸頁
.formLogin().loginPage("/login")
// 設置登陸成功頁
.defaultSuccessUrl("/").permitAll()
// 自定義登陸用戶名和密碼參數,默認爲username和password
// .usernameParameter("username")
// .passwordParameter("password")
.and()
.logout().permitAll();
// 關閉CSRF跨域
http.csrf().disable();
}
@Override
public void configure(WebSecurity web) throws Exception {
// 設置攔截忽略文件夾,可以對靜態資源放行
web.ignoring().antMatchers(
"/**/**.gif",
"/**/**.jpg",
"/**/**.css",
"/**/**.jq",
"/**/**.js",
"/**/**.ttf",
"/**/**.woff",
"/**/**.woff2",
"/**/**.png");
}
}
9.運行測試
1.啓動工程之前,由於數據庫用戶表的密碼初始化的是明文,這裏我們需要使用SpringSecurity 提供的加密工具類對密碼進行重新加密修改。
加密類:
public class SpringSecurityUtil {
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("123");
System.out.println(password);
}
}
加密後密文:$2az/GksRmh.CxJbP3nAiFWUu0yLfJ6YvUL04OxttZXBnClvEbU3KQgy
注:隨機鹽加密所以兩次加密後結果不一樣
2.啓動工程:
ROLE_ADMIN 賬戶:用戶名: admin,密碼: 123
ROLE_USER 賬戶:用戶名: user,密碼: 123
3.網頁顯示