一、什麼是SpringSecurity
Spring Security是一個能夠爲基於Spring的企業應用系統提供聲明式的安全訪問控制解決方案的安全框架。它提供了一組可以在Spring應用上下文中配置的Bean,充分利用了Spring IoC,DI(控制反轉Inversion of Control ,DI:Dependency Injection 依賴注入)和AOP(面向切面編程)功能,爲應用系統提供聲明式的安全訪問控制功能,減少了爲企業系統安全控制編寫大量重複代碼的工作。
二、SpringBoot2.0以上版本整合SpringSecurity
依賴
<!-- 管理依賴 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.M7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- SpringBoot整合Web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- springboot整合freemarker -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!--spring-boot 整合security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
</dependencies>
<!-- 注意: 這裏必須要添加, 否者各種依賴有問題 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
application.yml
# 配置freemarker
spring:
freemarker:
# 設置模板後綴名
suffix: .ftl
# 設置文檔類型
content-type: text/html
# 設置頁面編碼格式
charset: UTF-8
# 設置頁面緩存
cache: false
# 設置ftl文件路徑
template-loader-path:
- classpath:/templates
# 設置靜態文件路徑,js,css等
mvc:
static-path-pattern: /static/**
三、SpringSecurity有兩種認證模式
3.1、HttpBasic模式
什麼是Basic認證
在HTTP協議進行通信的過程中,HTTP協議定義了基本認證過程以允許HTTP服務器對WEB瀏覽器進行用戶身份證的方法,當一個客戶端向HTTP服務器進行數據請求時,如果客戶端未被認證,則HTTP服務器將通過基本認證過程對客戶端的用戶名及密碼進行驗證,以決定用戶是否合法。客戶端在接收到HTTP服務器的身份認證要求後,會提示用戶輸入用戶名及密碼,然後將用戶名及密碼以BASE64加密,加密後的密文將附加於請求信息中, 如當用戶名爲mayikt,密碼爲:123456時,客戶端將用戶名和密碼用“:”合併,並將合併後的字符串用BASE64加密爲密文,並於每次請求數據 時,將密文附加於請求頭(Request Header)中。HTTP服務器在每次收到請求包後,根據協議取得客戶端附加的用戶信息(BASE64加密的用戶名和密碼),解開請求包,對用戶名及密碼進行驗證,如果用 戶名及密碼正確,則根據客戶端請求,返回客戶端所需要的數據;否則,返回錯誤代碼或重新要求客戶端提供用戶名及密碼。
編寫認證類SecurityConfig
@Component
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 用戶認證信息
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 設置用戶賬號信息和權限
auth.inMemoryAuthentication().withUser("admin").password("123456").authorities("addOrder");
}
// 配置HttpSecurity 攔截資源
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic();
}
}
啓動項目
訪問:127.0.0.1/login
SpringSecurity爲我們提供了一個簡單的登錄界面(後邊會講如何替換爲我們自己的登錄界面)
點擊"取消"報錯
There is no PasswordEncoder mapped for the id "null"
原因:升級爲Security5.0以上密碼支持多中加密方式
解決辦法:在上邊類裏添加如下代碼恢復加密方式即可
@Beanpublic static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
3.2、FromLogin模式
FromLogin模式是以表單形式進行認證的
將上邊的configure方法裏邊換成下邊這種即可
// 配置HttpSecurity 攔截資源
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().formLogin();
}
四、實現權限的分配
admin用戶可以訪問所有頁面,其他不同的用戶只能訪問屬於自己的頁面
package com.mayikt.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.crypto.password.NoOpPasswordEncoder;
import org.springframework.stereotype.Component;
import com.mayikt.handler.MyAuthenticationFailureHandler;
import com.mayikt.handler.MyAuthenticationSuccessHandler;
// Security 配置
@Component
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationFailureHandler failureHandler;
@Autowired
private MyAuthenticationSuccessHandler successHandler;
// 配置認證用戶信息和權限
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 添加admin賬號
auth.inMemoryAuthentication().withUser("admin").password("123456").
//注意:下邊的參數裏可以添加集合,即應該是從數據庫裏查出來轉爲集合放進這裏,而不是寫死
authorities("showOrder","addOrder","updateOrder","deleteOrder");
// 添加userAdd賬號,最後兩個參數"showOrder","addOrder"是權限名稱,,在下邊的方法裏定義的
auth.inMemoryAuthentication().withUser("userAdd").password("123456").authorities("showOrder","addOrder");
// 如果想實現動態賬號與數據庫關聯 在該地方改爲查詢數據庫
}
// 配置攔截請求資源
protected void configure(HttpSecurity http) throws Exception {
// 如何實現權限控制: 給每一個請求路徑 分配一個權限名稱 讓後賬號只要關聯該名稱,就可以有訪問權限
http.authorizeRequests()
// 配置查詢訂單權限:第一個參數"/showOrder"指的是請求路徑,第二個參數"showOrder"指的是權限名稱
.antMatchers("/showOrder").hasAnyAuthority("showOrder")
.antMatchers("/addOrder").hasAnyAuthority("addOrder")
.antMatchers("/login").permitAll()
.antMatchers("/updateOrder").hasAnyAuthority("updateOrder")
.antMatchers("/deleteOrder").hasAnyAuthority("deleteOrder")
.antMatchers("/**").fullyAuthenticated().and().formLogin().loginPage("/login").
successHandler(successHandler).failureHandler(failureHandler)
.and().csrf().disable();
}
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
}
此時userAdd用戶沒有刪除和更新的權限,它訪問刪除與更新的路徑時,報錯:
403:權限不足
401:沒有授權(沒有授任何權限)
這個錯誤提示頁面對於用戶來說是不友好的,要改成我們自己寫的頁面
修改403頁面
這裏是SpringBoot2.0以上版本配置SpringSecurity錯誤頁面(與SpringBoot2.0以下版本配置的方式是不一樣的)
定義一個配置類:
/**
* 自定義 WEB 服務器參數 可以配置默認錯誤頁面
*
* @author sgw
* @version 2019年12月25日
*/
@Configuration
public class WebServerAutoConfiguration {
@Bean
public ConfigurableServletWebServerFactory webServerFactory() {
TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
ErrorPage errorPage400 = new ErrorPage(HttpStatus.BAD_REQUEST, "/error/400");
ErrorPage errorPage401 = new ErrorPage(HttpStatus.UNAUTHORIZED, "/error/401");
ErrorPage errorPage403 = new ErrorPage(HttpStatus.FORBIDDEN, "/error/403");
ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error/404");
ErrorPage errorPage415 = new ErrorPage(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "/error/415");
ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500");
factory.addErrorPages(errorPage400, errorPage401, errorPage403, errorPage404, errorPage415, errorPage500);
return factory;
}
}
在相應的路徑放上我們自己的403頁面:
新建ErrorController:
@Controller
public class ErrorController {
@RequestMapping("/error/403")
public String error() {
return "/error/403";
}
}
五、替換爲我們自己的登錄界面
修改fromLogin模式登陸頁面
注意替換configure(HttpSecurity http) 方法的最後一行:改爲自己的登錄界面
@Component
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 用戶認證信息
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 設置用戶賬號信息和權限
auth.inMemoryAuthentication().withUser("admin").password("123456").authorities("showOrder","addOrder","updateOrder","deleteOrder");
// 添加 useradd賬號 只有添加查詢和添加訂單權限
auth.inMemoryAuthentication().withUser("userAdd").password("123456")
.authorities("showOrder","addOrder");
}
// 配置HttpSecurity 攔截資源
protected void configure(HttpSecurity http) throws Exception {
// // 攔截請求, 權限名稱
http.authorizeRequests()
.antMatchers("/showOrder").hasAnyAuthority("showOrder")
.antMatchers("/addOrder").hasAnyAuthority("addOrder")
//不能攔截登錄請求
.antMatchers("/login").permitAll()
.antMatchers("/updateOrder").hasAnyAuthority("updateOrder")
.antMatchers("/deleteOrder").hasAnyAuthority("deleteOrder")
//並且關閉csrf,禁止後就不需要傳token了(解決跨站點攻擊)
.antMatchers("/**").fullyAuthenticated().and().formLogin().loginPage("/login").and().csrf().disable();
}
// SpringBoot2.0拋棄了原來的NoOpPasswordEncoder,要求用戶保存的密碼必須要使用加密算法後存儲,在登錄驗證的時候Security會將獲得的密碼在進行編碼後再和數據庫中加密後的密碼進行對比
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
}
在Controller裏添加:
// 自定義登陸頁面
@GetMapping("/login")
public String login() {
return "login";
}
上邊的配置主要就是這兩個地方
注意:自定義的頁面裏這幾個值只能這樣寫,因爲SpringSecurity是根據這幾個值做判斷的
六、判斷是否登錄成功
自定義兩個類,分別實現下邊兩個接口即可
- AuthenticationFailureHandler 認證失敗接口
- AuthenticationSuccessHandler 認證成功接口
//認證失敗
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse res, AuthenticationException auth)
throws IOException, ServletException {
System.out.println("用戶認證失敗");
//跳到指定界面
res.sendRedirect("http://www.maltose.com");
}
}
//認證成功
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
// 用戶認證成功
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication auth)
throws IOException, ServletException {
System.out.println("用戶登陸成功");
//重定向到首頁
res.sendRedirect("/");
}
}
SecurityConfig類改造:
@Component
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthenticationSuccessHandler successHandler;
@Autowired
private MyAuthenticationFailureHandler failHandler;
// 用戶認證信息
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 設置用戶賬號信息和權限
auth.inMemoryAuthentication().withUser("admin").password("123456").authorities("showOrder","addOrder","updateOrder","deleteOrder");
// 添加 useradd賬號 只有添加查詢和添加訂單權限
auth.inMemoryAuthentication().withUser("userAdd").password("123456")
.authorities("showOrder","addOrder");
}
// 配置HttpSecurity 攔截資源
protected void configure(HttpSecurity http) throws Exception {
// // 攔截請求, 權限名稱
http.authorizeRequests()
.antMatchers("/showOrder").hasAnyAuthority("showOrder")
.antMatchers("/addOrder").hasAnyAuthority("addOrder")
.antMatchers("/login").permitAll()
.antMatchers("/updateOrder").hasAnyAuthority("updateOrder")
.antMatchers("/deleteOrder").hasAnyAuthority("deleteOrder")
//並且關閉csrf
.antMatchers("/**").fullyAuthenticated().and().formLogin().loginPage("/login").successHandler(successHandler).failureHandler(failHandler).and().csrf().disable();
}
// SpringBoot2.0拋棄了原來的NoOpPasswordEncoder,要求用戶保存的密碼必須要使用加密算法後存儲,在登錄驗證的時候Security會將獲得的密碼在進行編碼後再和數據庫中加密後的密碼進行對比
@Bean
public static NoOpPasswordEncoder passwordEncoder() {
return (NoOpPasswordEncoder) NoOpPasswordEncoder.getInstance();
}
}
上邊改的地方主要是:
七、RBAC權限模型
基於角色的權限訪問控制(Role-Based Access Control)作爲傳統訪問控制(自主訪問,強制訪問)的有前景的代替受到廣泛的關注。在RBAC中,權限與角色相關聯,用戶通過成爲適當角色的成員而得到這些角色的權限。這就極大地簡化了權限的管理。在一個組織中,角色是爲了完成各種工作而創造,用戶則依據它的責任和資格來被指派相應的角色,用戶可以很容易地從一個角色被指派到另一個角色。角色可依新的需求和系統的合併而賦予新的權限,而權限也可根據需要而從某角色中回收。角色與角色的關係可以建立起來以囊括更廣泛的客觀情況。
RBAC核心就是下邊這五張表
- 用戶角色表:實現一個用戶多個角色
- 角色權限表:實現一個角色有多個權限
上邊五張表的建表語句
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for sys_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_permission`;
CREATE TABLE `sys_permission` (
`id` int(10) NOT NULL,
`permName` varchar(50) DEFAULT NULL,
`permTag` varchar(50) DEFAULT NULL,
`url` varchar(255) DEFAULT NULL COMMENT '請求url',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_permission
-- ----------------------------
INSERT INTO `sys_permission` VALUES ('1', '查詢訂單', 'showOrder', '/showOrder');
INSERT INTO `sys_permission` VALUES ('2', '添加訂單', 'addOrder', '/addOrder');
INSERT INTO `sys_permission` VALUES ('3', '修改訂單', 'updateOrder', '/updateOrder');
INSERT INTO `sys_permission` VALUES ('4', '刪除訂單', 'deleteOrder', '/deleteOrder');
-- ----------------------------
-- Table structure for sys_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` int(10) NOT NULL,
`roleName` varchar(50) DEFAULT NULL,
`roleDesc` varchar(50) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_role
-- ----------------------------
INSERT INTO `sys_role` VALUES ('1', 'admin', '管理員');
INSERT INTO `sys_role` VALUES ('2', 'add_user', '添加管理員');
-- ----------------------------
-- Table structure for sys_role_permission
-- ----------------------------
DROP TABLE IF EXISTS `sys_role_permission`;
CREATE TABLE `sys_role_permission` (
`role_id` int(10) DEFAULT NULL,
`perm_id` int(10) DEFAULT NULL,
KEY `FK_Reference_3` (`role_id`),
KEY `FK_Reference_4` (`perm_id`),
CONSTRAINT `FK_Reference_4` FOREIGN KEY (`perm_id`) REFERENCES `sys_permission` (`id`),
CONSTRAINT `FK_Reference_3` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_role_permission
-- ----------------------------
INSERT INTO `sys_role_permission` VALUES ('1', '1');
INSERT INTO `sys_role_permission` VALUES ('1', '2');
INSERT INTO `sys_role_permission` VALUES ('1', '3');
INSERT INTO `sys_role_permission` VALUES ('1', '4');
INSERT INTO `sys_role_permission` VALUES ('2', '1');
INSERT INTO `sys_role_permission` VALUES ('2', '2');
-- ----------------------------
-- Table structure for sys_user
-- ----------------------------
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` int(10) NOT NULL,
`username` varchar(50) DEFAULT NULL,
`realname` varchar(50) DEFAULT NULL,
`password` varchar(50) DEFAULT NULL,
`createDate` date DEFAULT NULL,
`lastLoginTime` date DEFAULT NULL,
`enabled` int(5) DEFAULT NULL,
`accountNonExpired` int(5) DEFAULT NULL,
`accountNonLocked` int(5) DEFAULT NULL,
`credentialsNonExpired` int(5) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_user
-- ----------------------------
INSERT INTO `sys_user` VALUES ('1', 'admin', '張三', '15a013bcac0c50049356b322e955035e\r\n', '2018-11-13', '2018-11-13', '1', '1', '1', '1');
INSERT INTO `sys_user` VALUES ('2', 'userAdd', '小余', '15a013bcac0c50049356b322e955035e\r\n', '2018-11-13', '2018-11-13', '1', '1', '1', '1');
-- ----------------------------
-- Table structure for sys_user_role
-- ----------------------------
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`user_id` int(10) DEFAULT NULL,
`role_id` int(10) DEFAULT NULL,
KEY `FK_Reference_1` (`user_id`),
KEY `FK_Reference_2` (`role_id`),
CONSTRAINT `FK_Reference_2` FOREIGN KEY (`role_id`) REFERENCES `sys_role` (`id`),
CONSTRAINT `FK_Reference_1` FOREIGN KEY (`user_id`) REFERENCES `sys_user` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
-- ----------------------------
-- Records of sys_user_role
-- ----------------------------
INSERT INTO `sys_user_role` VALUES ('1', '1');
INSERT INTO `sys_user_role` VALUES ('2', '2');
根據下邊的SQL,即可查詢一個用戶有哪些權限:
最後一個參數就是用戶名;運行結果
代碼實現
添加依賴,整合Mybatis框架
<!-->spring-boot 整合security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- springboot 整合mybatis框架 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- alibaba的druid數據庫連接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
application.yml:
# 配置freemarker
spring:
freemarker:
# 設置模板後綴名
suffix: .ftl
# 設置文檔類型
content-type: text/html
# 設置頁面編碼格式
charset: UTF-8
# 設置頁面緩存
cache: false
# 設置ftl文件路徑
template-loader-path:
- classpath:/templates
# 設置靜態文件路徑,js,css等
mvc:
static-path-pattern: /static/**
####整合數據庫層
datasource:
name: test
url: jdbc:mysql://127.0.0.1:3306/rbac_db
username: root
password: root
# druid 連接池
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.jdbc.Driver
新建Mapper:
public interface UserMapper {
// 查詢用戶信息
@Select(" select * from sys_user where username = #{userName}")
User findByUsername(@Param("userName") String userName);
// 查詢用戶的權限
@Select(" select permission.* from sys_user user" + " inner join sys_user_role user_role"
+ " on user.id = user_role.user_id inner join "
+ "sys_role_permission role_permission on user_role.role_id = role_permission.role_id "
+ " inner join sys_permission permission on role_permission.perm_id = permission.id where user.username = #{userName};")
List<Permission> findPermissionByUsername(@Param("userName") String userName);
}
public interface PermissionMapper {
// 查詢蘇所有權限
@Select(" select * from sys_permission ")
List<Permission> findAllPermission();
}
注意:User類必須實現UserDetail接口
package com.mayikt.entity;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import lombok.Data;
// 用戶信息表 @Data:插件的get/set方法
@Data
public class User implements UserDetails {
private Integer id;
private String username;
private String realname;
private String password;
private Date createDate;
private Date lastLoginTime;
private boolean enabled;
private boolean accountNonExpired;
private boolean accountNonLocked;
private boolean credentialsNonExpired;
// 用戶所有權限
private List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
}
package com.mayikt.entity;
import lombok.Data;
// 角色信息表
@Data
public class Role {
private Integer id;
private String roleName;
private String roleDesc;
}
package com.mayikt.entity;
import lombok.Data;
@Data
public class Permission {
private Integer id;
// 權限名稱
private String permName;
// 權限標識
private String permTag;
// 請求url
private String url;
}
重點:將用戶的權限查詢改爲動態查詢數據庫(之前是代碼裏寫死的)
注意:下邊新建的類實現了UserDetailsService 接口,其中,驗證密碼是否正確不需要我們自己做,SpringSecurity框架幫我們去驗證了
package com.mayikt.security;
import java.util.ArrayList;
import java.util.List;
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.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import com.mayikt.entity.Permission;
import com.mayikt.entity.User;
import com.mayikt.mapper.UserMapper;
@Component
public class MyUserDetailService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
// 查詢用戶信息
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 1、根據用戶名查詢用戶信息
User user = userMapper.findByUsername(username);
// 2、底層會根據用戶名查詢用戶信息,驗證密碼是否正確
//3、給用戶設置權限
List<Permission> listPermission = userMapper.findPermissionByUsername(username);
for (Permission permission : listPermission) {
authorities.add(new SimpleGrantedAuthority(permission.getPermTag()));
}
// 設置用戶權限
user.setAuthorities(authorities);
return user;
}
}
改造之前寫的用戶權限校驗的類的這個方法:
@Autowired
private MyUserDetailService myUserDetailsService;
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// // 添加admin賬號
// auth.inMemoryAuthentication().withUser("admin").password("123456").
// authorities("showOrder","addOrder","updateOrder","deleteOrder");
// // 添加userAdd賬號
// auth.inMemoryAuthentication().withUser("userAdd").password("123456").authorities("showOrder","addOrder");
// // 如果想實現動態賬號與數據庫關聯 在該地方改爲查詢數據庫
auth.userDetailsService(myUserDetailsService);
}
使用MD5對密碼進行加密(MD5是單項加密,記得需要撒鹽,不然容易被暴力破解)
新建加密工具類:
package com.mayikt.utils;
import java.security.MessageDigest;
public class MD5Util {
//鹽
private static final String SALT = "mayikt";
public static String encode(String password) {
password = password + SALT;
MessageDigest md5 = null;
try {
md5 = MessageDigest.getInstance("MD5");
} catch (Exception e) {
throw new RuntimeException(e);
}
char[] charArray = password.toCharArray();
byte[] byteArray = new byte[charArray.length];
for (int i = 0; i < charArray.length; i++)
byteArray[i] = (byte) charArray[i];
byte[] md5Bytes = md5.digest(byteArray);
StringBuffer hexValue = new StringBuffer();
for (int i = 0; i < md5Bytes.length; i++) {
int val = ((int) md5Bytes[i]) & 0xff;
if (val < 16) {
hexValue.append("0");
}
hexValue.append(Integer.toHexString(val));
}
return hexValue.toString();
}
public static void main(String[] args) {
//輸出的是123456加密後的值
System.out.println(MD5Util.encode("123456"));
}
}
對用戶密碼加密(對SecurityConfig類的這個方法進行改造):
auth.userDetailsService(myUserDetailsService).passwordEncoder(new PasswordEncoder() {
//驗證密碼 rawPassword:表單提交的密碼(未加密) encodedPassword:數據庫裏的密碼(已加密)
public boolean matches(CharSequence rawPassword, String encodedPassword) {
//將表單提交的密碼進行加密
String rawPass= MD5Util.encode((String)rawPassword);
//將表單提交的密碼加密後,與數據庫的密碼比對,一致的話返回true,即認證成功
boolean result = rawPass.equals(encodedPassword);
return result;
}
// 對錶單提交來的密碼進行加密
public String encode(CharSequence rawPassword) {
return MD5Util.encode((String)rawPassword);
}
});
攔截請求資源做成動態的
之前是寫死的:
動態的:
@Autowired
private PermissionMapper permissionMapper;
@Autowired
private MyUserDetailService myUserDetailService;
// 配置攔截請求資源
protected void configure(HttpSecurity http) throws Exception {
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests = http
.authorizeRequests();
// 查詢數據庫獲取權限列表
List<Permission> listPermission = permissionMapper.findAllPermission();
for (Permission permission : listPermission) {
//設置權限
authorizeRequests.antMatchers(permission.getUrl()).hasAuthority(permission.getPermTag());
}
authorizeRequests.antMatchers("/login").permitAll().antMatchers("/**").fullyAuthenticated().and().formLogin()
.loginPage("/login").successHandler(successHandler).failureHandler(failureHandler).and().csrf()
.disable();
}
八、微服務開放平臺(Oauth2.0)接口原理
什麼是開放平臺
在一些大型互聯網公司,隨着公司的業務發展逐漸龐大,需要和外部合夥夥伴進行合作,需要將公司的接口開放給外部其他合夥夥伴進行調用。
比如騰訊的QQ互聯網、微信開放平臺、螞蟻金服開放平臺 、微博開放平臺,比如實現功能QQ聯合登陸、微信掃碼登陸。
還有就是在大型集團公司中,分爲總公司,和旗下多個分公司,總公司與分公司相互通訊也可以採用開放平臺形式對接口進行授權。
什麼是Oauth2.0認證授權協議
OAuth: OAuth(開放授權)是一個開放標準,允許用戶授權第三方網站訪問他們存儲在另外的服務提供者上的信息,而不需要將用戶名和密碼提供給第三方網站或分享他們數據的所有內容。
QQ登錄OAuth2.0:對於用戶相關的OpenAPI(例如獲取用戶信息,動態同步,照片,日誌,分享等),爲了保護用戶數據的安全和隱私,第三方網站訪問用戶數據前都需要顯式的向用戶徵求授權。
QQ登錄OAuth2.0採用OAuth2.0標準協議來進行用戶身份驗證和獲取用戶授權,相對於之前的OAuth1.0協議,其認證流程更簡單和安全。
常用開放平臺
QQ互聯網開放平臺(第三方登錄使用QQ登錄)
http://wiki.connect.qq.com/%E5%87%86%E5%A4%87%E5%B7%A5%E4%BD%9C_oauth2-0
Oauth2.0認證授權協議涉及到的幾個概念
- appId:商戶號 (QQ爲我們生成的值,永久不變)
- appKey:商戶祕鑰 (會變化的)
- 授權碼Code:通過授權碼獲取accessToken
- accessToken:調用接口權限 訪問令牌
- 回調地址:授權成功後,重定向的地址
- openId:開放平臺生產的唯一ID(對外公開的)
QQ聯合登錄流程
我們通過了解QQ聯合登錄,來學習項目裏應該如何提供接口給第三方登錄;
- 生成授權CODE鏈接 獲取授權碼
- 使用獲取到的授權碼,獲取對應的accessToken
- 使用accessToken獲取用戶的openId
- 使用openId獲取用戶相關信息
生成授權CODE鏈接
點擊下邊的QQ圖標時生成的連接
例如:
https://graph.qq.com/oauth2.0/authorize?response_type=code&client_id=101462456&state=888&redirect_uri=http://mayikt.s1.natapp.cc/qqLoginBack
QQ平臺要求URL需要的參數:
使用獲取到的授權碼去獲取對應的accessToken
例如:
https://graph.qq.com/oauth2.0/token?grant_type=authorization_code&client_id=101462456&client_secret=4488033be77331e7cdcaed8ceadc10d5&code=E91DF5B0E2455A6B2AF25CD9FA1C7582&
redirect_uri=http://mayikt.s1.natapp.cc/qqLoginBack
使用accessToken獲取用戶openid
https://graph.qq.com/oauth2.0/me?access_token=B2FD1997D149313F16C93D91C75AC75E
使用openid獲取用戶信息
https://graph.qq.com/user/get_user_info?access_token=B2FD1997D149313F16C93D91C75AC75E&oauth_consumer_key=101462456&openid=4B1717CBBFE1E900D2A1482C4A18B3BD
Oauth2.0流程總結
OAuth認證和授權的過程如下:
- 用戶訪問第三方網站網站,想對用戶存放在服務商的某些資源進行操作。
- 第三方網站向服務商請求一個臨時令牌。
- 服務商驗證第三方網站的身份後,授予一個臨時令牌。
- 第三方網站獲得臨時令牌後,將用戶導向至服務商的授權頁面請求用戶授權,然後這個過程中將臨時令牌和第三方網站的返回地址發送給服務商。
- 用戶在服務商的授權頁面上輸入自己的用戶名和密碼,授權第三方網站訪問所相應的資源。
- 授權成功後,服務商將用戶導向第三方網站的返回地址。
- 第三方網站根據臨時令牌從服務商那裏獲取訪問令牌。
- 服務商根據令牌和用戶的授權情況授予第三方網站訪問令牌。
- 第三方網站使用獲取到的訪問令牌訪問存放在服務商的對應的用戶資源。
九、搭建SpringCloud Oauth2.0
SpringCloud Oauth2作用:
管理微服務中的開放接口,對接口進行授權認證,使用協議就是SpringCloud Oauth2.0;
在Spring Cloud需要使用oauth2來實現多個微服務的統一認證授權,通過向OAUTH服務發送某個類型的grant type進行集中認證和授權,從而獲得access_token,而這個token是受其他微服務信任的,我們在後續的訪問可以通過access_token來進行,從而實現了微服務的統一認證授權。
客戶端根據約定的ClientID、ClientSecret、Scope來從Access Token URL地址獲取AccessToken,並經過AuthURL認證,用得到的AccessToken來訪問其他資源接口。
Spring Cloud oauth2 需要依賴Spring security
OAuth2四種授權方式
- 授權碼模式(authorization code)用在客戶端與服務端應用之間授權
- 簡化模式(implicit)用在移動app或者web app(這些app是在用戶的設備上的,如
在手機上調起微信來進行認證授權) - 密碼模式(resource owner password credentials)應用直接都是受信任的(都是由一家公司開發的)
- 客戶端模式(client credentials)用在應用API訪問
Oauth2角色劃分
5. Resource Server:被授權訪問的資源
6. Authotization Server:OAUTH2認證授權中心
7. Resource Owner: 用戶
8. Client:使用API的客戶端(如Android 、IOS、web app)
SpringCloud Oauth2.0原理圖:
SpringBoot2.0以上版本的OAuth2.0環境搭建
新建兩個項目
- oauth2-server:OAUTH2認證授權中心服務
- order_service: 普通微服務,驗證授權服務
oauth2-server認證授權中心服務
密碼模式:
依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<!-- 管理依賴 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.M7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- SpringBoot整合Web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- springboot整合freemarker -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-->spring-boot 整合security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- spring-cloud-starter-oauth2 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
<!-- 注意: 這裏必須要添加, 否者各種依賴有問題 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
創建配置信息
// 配置授權中心配置信息
@Configuration
@EnableAuthorizationServer // 開啓認證授權中心
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
// accessToken有效期
private int accessTokenValiditySeconds = 7200; // 兩小時
// 添加商戶信息
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// withClient appid申請獲取appId和appKey(這裏先寫死)
clients.inMemory().withClient("client_1").secret(passwordEncoder().encode("123456"))
.authorizedGrantTypes("password","client_credentials","refresh_token").scopes("all").accessTokenValiditySeconds(accessTokenValiditySeconds);
}
// 設置token類型
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager()).allowedTokenEndpointRequestMethods(HttpMethod.GET,
HttpMethod.POST);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
// 允許表單認證
oauthServer.allowFormAuthenticationForClients();
// 允許check_token訪問
oauthServer.checkTokenAccess("permitAll()");
}
@Bean
AuthenticationManager authenticationManager() {
AuthenticationManager authenticationManager = new AuthenticationManager() {
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
return daoAuhthenticationProvider().authenticate(authentication);
}
};
return authenticationManager;
}
@Bean
public AuthenticationProvider daoAuhthenticationProvider() {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(userDetailsService());
daoAuthenticationProvider.setHideUserNotFoundExceptions(false);
daoAuthenticationProvider.setPasswordEncoder(passwordEncoder());
return daoAuthenticationProvider;
}
// 設置添加用戶信息,正常應該從數據庫中讀取
@Bean
UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(User.withUsername("user_1").password(passwordEncoder().encode("123456"))
.authorities("ROLE_USER").build());
userDetailsService.createUser(User.withUsername("user_2").password(passwordEncoder().encode("1234567"))
.authorities("ROLE_USER").build());
return userDetailsService;
}
@Bean
PasswordEncoder passwordEncoder() {
// 加密方式
PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
return passwordEncoder;
}
}
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager()).allowedTokenEndpointRequestMethods(HttpMethod.GET,
HttpMethod.POST);
endpoints.authenticationManager(authenticationManager());
endpoints.userDetailsService(userDetailsService());
}
授權碼模式:
需要添加Security權限:
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 授權中心管理器
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
AuthenticationManager manager = super.authenticationManagerBean();
return manager;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 攔截所有請求,使用httpBasic方式登陸
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/**").fullyAuthenticated().and().httpBasic();
}
}
啓動授權服務
@SpringBootApplication
public class AppOauth2 {
public static void main(String[] args) {
SpringApplication.run(AppOauth2.class, args);
}
}
訪問:
http://localhost:8080/oauth/authorize?response_type=code&client_id=client_1&redirect_uri=http://www.mayikt.com
資源服務端
要訪問資源服務器受保護的資源需要攜帶令牌(從授權服務器獲得)
客戶端往往同時也是一個資源服務器,各個服務之間的通信(訪問需要權限的資源)時需攜帶訪問令牌;
資源服務器通過 @EnableResourceServer 註解來開啓一個 OAuth2AuthenticationProcessingFilter 類型的過濾器
通過繼承 ResourceServerConfigurerAdapter 類來配置資源服務器
新建項目order_service
maven
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<!-- 管理依賴 -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Finchley.M7</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<!-- SpringBoot整合Web組件 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- springboot整合freemarker -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
<!-->spring-boot 整合security -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
</dependencies>
<!-- 注意: 這裏必須要添加, 否者各種依賴有問題 -->
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/libs-milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
application.yml
server:
port: 8081
logging:
level:
org.springframework.security: DEBUG
security:
oauth2:
resource:
####從認證授權中心上驗證token
tokenInfoUri: http://localhost:8080/oauth/check_token
preferTokenInfo: true
client:
accessTokenUri: http://localhost:8080/oauth/token
userAuthorizationUri: http://localhost:8080/oauth/authorize
###appid
clientId: client_1
###appSecret
clientSecret: 123456
資源攔截配置
@Configuration
@EnableResourceServer
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
// 對 api/order 請求進行攔截
http.authorizeRequests().antMatchers("/api/order/**").authenticated();
}
}
資源服務請求
@RestController
@RequestMapping("/api/order")
public class OrderController {
@RequestMapping("/addOrder")
public String addOrder() {
return "addOrder";
}
}
啓動:
@SpringBootApplication
@EnableOAuth2Sso
public class AppOrder {
public static void main(String[] args) {
SpringApplication.run(AppOrder.class, args);
}
}
將應用信息改爲數據庫存儲
官方推薦SQL:
https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
-用於使用HSQL的測試中
創建 表 oauth_client_details(
client_id VARCHAR(256)主 鍵,
resource_ids VARCHAR(256),
client_secret VARCHAR(256),
範圍VARCHAR(256),
Authorized_grant_types VARCHAR(256),
web_server_redirect_uri VARCHAR(256),
權限VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additional_information VARCHAR(4096),
自動批准VARCHAR(256)
);
創建 表 oauth_client_token(
token_id VARCHAR(256),
令牌LONGVARBINARY,
authentication_id VARCHAR(256)PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256)
);
創建 表 oauth_access_token(
token_id VARCHAR(256),
令牌LONGVARBINARY,
authentication_id VARCHAR(256)PRIMARY KEY,
user_name VARCHAR(256),
client_id VARCHAR(256),
身份驗證 LONGVARBINARY,
refresh_token VARCHAR(256)
);
創建 表 oauth_refresh_token(
token_id VARCHAR(256),
令牌LONGVARBINARY,
認證 LONGVARBINARY
);
創建 表 oauth_code(
代碼VARCHAR(256),身份驗證 LONGVARBINARY
);
創建 表 oauth_approvals(
userId VARCHAR(256),
clientId VARCHAR(256),
範圍VARCHAR(256),
狀態 VARCHAR(10),
在TIMESTAMP到期,
lastModifiedAt TIMESTAMP
);
-定製的oauth_client_details表
創建 表 ClientDetails(
appId VARCHAR(256)主 鍵,
resourceIds VARCHAR(256),
appSecret VARCHAR(256),
範圍VARCHAR(256),
grantTypes VARCHAR(256),
redirectUrl VARCHAR(256),
權限VARCHAR(256),
access_token_validity INTEGER,
refresh_token_validity INTEGER,
additionalInformation VARCHAR(4096),
autoApproveScopes VARCHAR(256)
);
修改認證授權中心
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
application.yml
spring:
datasource:
hikari:
connection-test-query: SELECT 1
minimum-idle: 1
maximum-pool-size: 5
pool-name: dbcp1
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/alan-oauth?autoReconnect=true&useSSL=false
username: root
password: root
修改配置文件類
// 配置授權中心信息
@Configuration
@EnableAuthorizationServer // 開啓認證授權中心
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
@Autowired
@Qualifier("dataSource")
private DataSource dataSource;
// @Autowired
// private UserDetailsService userDetailsService;
@Bean
public TokenStore tokenStore() {
// return new InMemoryTokenStore(); //使用內存中的 token store
return new JdbcTokenStore(dataSource); /// 使用Jdbctoken store
}
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 添加授權用戶
clients.jdbc(dataSource);
// .withClient("client_1").secret(new BCryptPasswordEncoder().encode("123456"))
// .authorizedGrantTypes("password", "refresh_token", "authorization_code")// 允許授權範圍
// .redirectUris("http://www.mayikt.com").authorities("ROLE_ADMIN", "ROLE_USER")// 客戶端可以使用的權限
// .scopes("all").accessTokenValiditySeconds(7200).refreshTokenValiditySeconds(7200);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager)
.userDetailsService(userDetailsService());// 必須設置
// UserDetailsService
// 否則刷新token 時會報錯
}
@Bean
UserDetailsService userDetailsService() {
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager();
userDetailsService.createUser(User.withUsername("user_1").password(new BCryptPasswordEncoder().encode("123456"))
.authorities("ROLE_USER").build());
userDetailsService.createUser(User.withUsername("user_2")
.password(new BCryptPasswordEncoder().encode("1234567")).authorities("ROLE_USER").build());
return userDetailsService;
}
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
security.tokenKeyAccess("permitAll()").checkTokenAccess("isAuthenticated()")
.allowFormAuthenticationForClients();// 允許表單登錄
}
}