Spring Security進階

1.連線資料庫進行資料的驗證

Spring Security進行身份驗證或者許可權控制時,使用者名稱和密碼應該要和資料庫的進行比較才行,使用者的各種資訊我們從資料庫中去獲取,不用自己在程式碼或者配置檔案中寫。

案例

1)建立專案

自己建立一個Maven專案

2)匯入依賴

<parent>
<!--Spring boot-->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-parent</artifactId>
<version>2.0.6.RELEASE</version>
</parent> <dependencies>
<!--Spring boot Web-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency> <!--Spring 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> <!--資料庫連線框架JPA-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
</dependencies>

3)建立啟動類

@SpringBootApplication
public class SecurityApplication2 {
public static void main(String[] args) {
SpringApplication.run(SecurityApplication2.class, args);
}
}

4)配置檔案

在專案的resources檔案的目錄下建立一個配置檔案:application.properties

#連線資料庫
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/你的資料庫?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=使用者名稱
spring.datasource.password=密碼
#資料庫表的生成
spring.jpa.generate-ddl=true
#顯示執行的sql語句
spring.jpa.show-sql=true
#使用的資料庫
spring.jpa.database=mysql

5)建立類

來建立一個實體層類、dao層介面、service層、控制層

實體類

//這個註解表示這個是實體類,對應資料庫中的表,預設實體名就是類名
@Entity
public class UserInfo { //這個屬性的表中的主鍵
@Id
//主鍵的生成策略,IDENTITY 主鍵自增長
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//使用者名稱
private String username;
//密碼
private String password;
//角色
private String roles; public Long getId() {
return id;
} public void setId(Long 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;
} public String getRoles() {
return roles;
} public void setRoles(String roles) {
this.roles = roles;
}
}

dao層介面

/*
* 繼承JpaRepository<UserInfo,Long>介面,泛型一個是實體類,一個是主鍵型別
* */
public interface UserInfoDao extends JpaRepository<UserInfo,Long> {
//根據使用者名稱查詢使用者
//(溫馨提示:想要查詢的話方法名要為 findBy屬性欄位 刪除的話 deleteBy屬性欄位 ...以此類推這樣才不報錯)
UserInfo findByUsername(String username);
}

service層

public interface UserInfoService {
//根據使用者名稱查詢使用者
UserInfo findByUsername(String username);
}
@Service
public class UserInfoServiceImpl implements UserInfoService { @Autowired
private UserInfoDao userInfoDao; //根據使用者名稱返回使用者
@Override
public UserInfo findByUsername(String username) {
UserInfo user = userInfoDao.findByUsername(username);
return user;
}
}

controller層

@RestController
@RequestMapping("/userInfo")
public class UserInfoController { @RequestMapping("common")
//方法執行前驗證使用者是否有該角色
@PreAuthorize(value = "hasAnyRole('normal','admin')")
public String commonUserInfo() {
return "==測試資料庫 有兩個角色==";
} @RequestMapping("admin")
//方法執行前驗證是否有該角色
@PreAuthorize(value = "hasAnyRole('admin')")
public String adminUserInfo() {
return "==測試資料庫 有一個角色==";
}
}

這些準備工作都做完之後,我們可以往資料庫中插入一些資料,建立一個類,用來往資料庫中新增資料的。

@Component
public class InitJdbc { @Autowired
private UserInfoDao userInfoDao; //Java自帶的註解,程式啟動的時候執行該方法,每啟動一次執行一次,
//插入成功了,再啟動專案的時候不想再重複插入,可以把@PostConstruct註釋掉
@PostConstruct
public void init() {
//密碼要加密
PasswordEncoder pe = new BCryptPasswordEncoder(); //新增一個使用者
UserInfo userInfo = new UserInfo();
userInfo.setUsername("wangwu");
userInfo.setPassword(pe.encode("123456"));
userInfo.setRoles("normal");
userInfoDao.save(userInfo); //新增一個使用者
UserInfo userInfo1 = new UserInfo();
userInfo1.setUsername("admin");
userInfo1.setPassword(pe.encode("admin"));
userInfo1.setRoles("admin");
userInfoDao.save(userInfo1); }
}

現在就可以啟動專案看看資料庫中是否插入成功資料了。

接下來做許可權的驗證==========

來寫一個類,實現UserDetailService並實現它的方法

//把類交給spring容器管理
@Component
public class SecurityDetail implements UserDetailsService { @Autowired
private UserInfoDao userInfoDao; @Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { UserInfo userInfo = null; //UserDetails介面的實現類
User user = null;
//判斷使用者名稱不為空
if (s != null) {
//根據使用者名稱查詢使用者
userInfo = userInfoDao.findByUsername(s);
//判斷使用者不為空
if (userInfo != null) {
//User類的第三個引數是集合,所有建立一個集合,獲取使用者的角色
List<GrantedAuthority> list = new ArrayList<>();
//獲取使用者的角色,獲取到的角色開頭一定要以"ROLE_"開頭
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + userInfo.getRoles());
list.add(authority);
//User的構造器要返回三個引數:使用者名稱、密碼、集合的角色
user = new User(userInfo.getUsername(), userInfo.getPassword(), list);
}
} return user;
}
}

再寫一個配置類,解析密碼的加密方式

@Configuration
@EnableWebSecurity
//開啟方法級別的驗證
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired
private SecurityDetail securityDetail; @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//密碼加密的方式
auth.userDetailsService(securityDetail).passwordEncoder(new BCryptPasswordEncoder()); }
}

現在可以進行測試了,啟動專案,訪問控制層的方法,只有方法上標記的角色才能訪問該方法

2.認證和授權

authentication:認證,認證訪問的使用者是不是有效的使用者,他是誰。

authorization:授權,訪問的使用者在系統中能幹什麼

RBAC:基於角色的訪問控制(Role-Based Access Control),使用者屬於某個角色,而角色擁有某些許可權。

許可權:能對資源進行操作,比如增刪改查

角色:自定義的,表示許可權的聚合,一個角色可以有多個許可權。

舉例說明

設計角色:經理具有資料的修改、刪除、檢視等;員工只能檢視資料。

一個公司中如果想把一個使用者設定為經理,只需把他設定為經理這個角色,他就能有修改、刪除、檢視等操作了,如果公司新來普通員工,只需把他加入到員工這個角色裡面就好了。這樣,想讓什麼使用者使用者什麼許可權,只需把他加入到相應的角色裡就OK了。

UserDetailService:這是一個介面,裡面只有一個方法UserDetails loadUserByUsername(String var1),是根據使用者名稱來獲取資料庫中資訊的

主要的實現有:

InMemoryUserDetailsManager在記憶體中維護使用者資訊的,使用很方便,可是資料不是持久的

JdbcUserDetailsManager對資料庫資訊進行操作的,底層是基於jdbcTemplate的,可以使用這個類的方法來操作資料庫資料。

UserDetails:提供使用者資訊的核心介面

// 許可權的集合
Collection<? extends GrantedAuthority> getAuthorities();
//獲取密碼
String getPassword();
//獲取使用者名稱
String getUsername();
//使用者是否存在
boolean isAccountNonExpired();
//使用者是否鎖定
boolean isAccountNonLocked();
//證書是否過期
boolean isCredentialsNonExpired();
//賬戶是否啟用
boolean isEnabled();

UserDetails有一個實現類User

//他有兩個構造器,引數和UserDetails的欄位屬性一樣
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
} public User(String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {
if (username != null && !"".equals(username) && password != null) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = Collections.unmodifiableSet(sortAuthorities(authorities));
} else {
throw new IllegalArgumentException("Cannot pass null or empty values to constructor");
}
}

2.1設定表

基於RBAC設定三張表,使用者表,角色表,使用者和角色的關聯表 密碼明文分別是 123 456 admin



-- 角色表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0; -- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(11) NOT NULL,
`rolename` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色名稱',
`rolememo` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES (1, 'USER', '普通使用者');
INSERT INTO `sys_role` VALUES (2, 'READ', '只讀');
INSERT INTO `sys_role` VALUES (3, 'ADMIN', '管理員'); SET FOREIGN_KEY_CHECKS = 1; -- 使用者表 SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0; -- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`realname` varchar(200) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '真實名字',
`isenable` int(11) NULL DEFAULT NULL COMMENT '是否開啟認證',
`islock` int(11) NULL DEFAULT NULL COMMENT '是否鎖定',
`isexpire` int(11) NULL DEFAULT NULL,
`incredentials` int(255) NULL DEFAULT NULL COMMENT '是否過期',
`createtime` date NULL DEFAULT NULL,
`logintime` date NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ----------------------------
-- Records of sys_user 密碼明文分別是 123 456 admin
-- ----------------------------
INSERT INTO `sys_user` VALUES (6, 'zs', '$2a$10$EGMo2XSdh49cDgXa0OzXYu36HfNssUf7zUDaNIz83AgWveA3GORYq', '張三', 1, 1, 1, 1, '2021-09-02', '2021-09-02');
INSERT INTO `sys_user` VALUES (7, 'lisi', '$2a$10$r9iLBYZzIIt/gyOngvPnZOBZaP4EW58etU1tLPoEh7hlYpydIaM6u', '李四', 1, 1, 1, 1, '2021-09-02', '2021-09-02');
INSERT INTO `sys_user` VALUES (8, 'admin', '$2a$10$P.I3zf7bEAmLmlSwaDOdMOdrxEyTT1QvbqfKC5YGQ7zHk5zUR/dCG', '管理員', 1, 1, 1, 1, '2021-09-02', '2021-09-02'); SET FOREIGN_KEY_CHECKS = 1; --關聯表
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0; -- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`userid` int(11) NOT NULL,
`roleid` int(11) NULL DEFAULT NULL
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES (6, 1);
INSERT INTO `sys_user_role` VALUES (7, 2);
INSERT INTO `sys_user_role` VALUES (8, 1);
INSERT INTO `sys_user_role` VALUES (8, 3); SET FOREIGN_KEY_CHECKS = 1;

2.2建立專案

2.3 匯入依賴

把上一個專案的依賴匯入進來,再加多一個spring整合mybatis的包

 <dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>

2.4建立相關類

2.4.1實體類

建立User和Role的實體類,要繼承UserDetails

public class SysUser implements UserDetails {

    private Integer id;
private String username;
private String password;
private String realName;
private boolean isEnable;
private boolean isExpired;
private boolean isLock;
private boolean isCredentials; private List<GrantedAuthority> grantedAuthorities; private Date createTime;
private Date loginTime; public SysUser() {
} public SysUser(String username, String password, String realName,
boolean isEnable, boolean isExpired, boolean isLock,
boolean isCredentials, List<GrantedAuthority> grantedAuthorities,
Date createTime, Date loginTime) {
this.username = username;
this.password = password;
this.realName = realName;
this.isEnable = isEnable;
this.isExpired = isExpired;
this.isLock = isLock;
this.isCredentials = isCredentials;
this.grantedAuthorities = grantedAuthorities;
this.createTime = createTime;
this.loginTime = loginTime;
} //角色的集合
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return grantedAuthorities;
} //密碼
@Override
public String getPassword() {
return password;
} //使用者名稱
@Override
public String getUsername() {
return username;
} //賬號是否存在
@Override
public boolean isAccountNonExpired() {
return isExpired;
} //賬號是否鎖定
@Override
public boolean isAccountNonLocked() {
return isLock;
} //是否過期
@Override
public boolean isCredentialsNonExpired() {
return isCredentials;
} //是否啟用
@Override
public boolean isEnabled() {
return isEnable;
} public void setId(Integer id) {
this.id = id;
} public void setUsername(String username) {
this.username = username;
} public void setPassword(String password) {
this.password = password;
} public void setRealName(String realName) {
this.realName = realName;
} public void setEnable(boolean enable) {
isEnable = enable;
} public void setExpired(boolean expired) {
isExpired = expired;
} public void setLock(boolean lock) {
isLock = lock;
} public void setCredentials(boolean credentials) {
isCredentials = credentials;
} public void setGrantedAuthorities(List<GrantedAuthority> grantedAuthorities) {
this.grantedAuthorities = grantedAuthorities;
} public void setCreateTime(Date createTime) {
this.createTime = createTime;
} public void setLoginTime(Date loginTime) {
this.loginTime = loginTime;
} public Integer getId() {
return id;
} public String getRealName() {
return realName;
} public Date getCreateTime() {
return createTime;
} public Date getLoginTime() {
return loginTime;
} @Override
public String toString() {
return "SysUser{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", realName='" + realName + '\'' +
", isEnable=" + isEnable +
", isExpired=" + isExpired +
", isLock=" + isLock +
", isCredentials=" + isCredentials +
", grantedAuthorities=" + grantedAuthorities +
", createTime=" + createTime +
", loginTime=" + loginTime +
'}';
}
}
public class SysRole {

    private Integer id;
private String role;
private String rolememo; public Integer getId() {
return id;
} public void setId(Integer id) {
this.id = id;
} public String getRole() {
return role;
} public void setRole(String role) {
this.role = role;
} public String getRolememo() {
return rolememo;
} public void setRolememo(String rolememo) {
this.rolememo = rolememo;
} @Override
public String toString() {
return "SysRole{" +
"id=" + id +
", role='" + role + '\'' +
", rolememo='" + rolememo + '\'' +
'}';
}
}
2.4.2dao層和對應的xml檔案
@Repository
public interface SysRoleMapper { //根據使用者id檢視角色
List<SysRole> selectByUserId(Integer userId);
} @Repository
public interface SysUserMapper { //插入使用者
int insertSysUser(SysUser sysUser); //根據使用者名稱查詢使用者
SysUser selectByUsername(String username);
}
<?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.huang.security.mapper.SysRoleMapper"> <resultMap id="roleMapper" type="com.huang.security.entity.SysRole">
<id column="id" property="id"/>
<result column="rolename" property="role"/>
<result column="rolememo" property="rolememo"/>
</resultMap> <select id="selectByUserId" resultMap="roleMapper" >
SELECT sr.id,sr.rolename,sr.rolememo FROM sys_role AS sr
INNER JOIN sys_user_role AS sur ON sr.id = sur.roleid
WHERE sur.userid = #{userid}
</select>
</mapper>
<?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.huang.security.mapper.SysUserMapper"> <resultMap id="userMapper" type="com.huang.security.entity.SysUser">
<id column="id" property="id"/>
<result column="username" property="username"/>
<result column="password" property="password"/>
<result column="realname" property="realName"/>
<result column="isenable" property="isEnable"/>
<result column="islock" property="isLock"/>
<result column="isexpire" property="isExpired"/>
<result column="incredentials" property="isCredentials"/>
<result column="createtime" property="createTime"/>
<result column="logintime" property="loginTime"/> </resultMap> <insert id="insertSysUser" parameterType="com.huang.security.entity.SysUser">
insert into sys_user(username,password,realname,isenable,islock,incredentials,createtime,logintime)
values(#{username},#{password},#{realName},#{isEnable},#{isLock},#{isCredentials},
#{createTime},#{loginTime})
</insert> <select id="selectByUsername" resultMap="userMapper" >
select id,username,password,realname,isenable,islock,isexpire,incredentials,createtime,logintime
from sys_user where username = #{username}
</select>
</mapper>
2.5service層

service層要實現UserDetailService介面,去獲取資料庫中的資訊做返回

@Service
public class UserWnoRoleService implements UserDetailsService { @Autowired
private SysUserMapper userMapper; @Autowired
private SysRoleMapper roleMapper; @Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//根據使用者名稱獲取使用者
SysUser user = userMapper.selectByUsername(username); System.out.println("==== Service =====");
String roleName = "";
List<GrantedAuthority> list = new ArrayList<>(); System.out.println("User" + user);
if (!StringUtils.isEmpty(user)) {
//根據使用者id獲取對應角色
List<SysRole> roles = roleMapper.selectByUserId(user.getId()); for (SysRole role : roles) {
//一個使用者可能有多個角色,用集合儲存,放到使用者的集合裡
roleName = role.getRole();
GrantedAuthority authority = new SimpleGrantedAuthority("ROLE_" + roleName);
list.add(authority);
user.setGrantedAuthorities(list);
} //返回的這個user是包含角色的
return user;
}
//可以返回自定義user,是因為實體類實現了UserDetails這個介面
return user;
}
}
2.6配置檔案
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/庫名?useUnicode=true&characterEncoding=utf-8&useSSL=false
spring.datasource.username=使用者名稱
spring.datasource.password=密碼 mybatis.mapper-locations=classpath:/mapper/*Mapper.xml
# 包起別名
mybatis.type-aliases-package=com.huang.security.entity
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
2.7相關的配置類
@Configuration
//@EnableWebSecurity //如果是匯入的jar包是spring-boot-starter-security可以不用寫
public class MySecurityConfig extends WebSecurityConfigurerAdapter { @Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("====== MySecurityConfig configure=============="); //匹配"/index","/login.html","/login" 不用驗證(permit 許可),和登入相關的要放行
http.authorizeRequests().antMatchers("/index","/login.html","/login").permitAll()
//匹配只有相關角色才能訪問的路徑
.antMatchers("/access/user/**").hasRole("USER")
.antMatchers("/access/read/**").hasRole("READ")
.antMatchers("/access/admin/**").hasRole("ADMIN")
//所有都需要驗證
.anyRequest().authenticated()
//執行結束
.and()
//表單的方式登入
.formLogin()
//登入的自定義檢視頁面
.loginPage("/login.html")
//登入訪問的地址,表單中action的值
.loginProcessingUrl("/login")
.and()
//跨域安全的設定,禁用
.csrf().disable();
} @Qualifier("userWnoRoleService")
@Autowired
private UserDetailsService userDetailsService; @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//super.configure(auth);
// userDetailsService使用的是service層的 UserWnoRoleService,它實現了 UserDetailsService
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
}
2.8html頁面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body> 身份驗證 <br>
<a href="/access/user">zs</a> <br>
<a href="/access/read">lisi</a> <br>
<a href="/access/admin">admin</a> <br>
<a href="/logout">退出</a>
</body>
</html> <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<p>自定義登入頁</p>
<form action="/login" method="post">
使用者名稱:<input type="text" name="username" value=""><br/>
密&nbsp;&nbsp;&nbsp;碼:<input type="password" name="password" value=""><br/>
<input type="submit" value="登入">
</form>
</body>
</html>
2.9controller層測試
@Controller
public class InitController { @GetMapping("index")
public String toIndex() {
return "forward:/index.html";
}
}
@RestController
@RequestMapping("/access")
public class UserWnoRoleController { @GetMapping("user")
public String sayUser() {
return "zs 是 user 角色";
} @GetMapping("read")
public String sayRead() {
return "lisi 是 read 角色";
} @GetMapping("admin")
public String sayAdmin() {
return "admin 是 user admin 角色";
}
}

個人筆記