菜鳥的spring security學習教程
說明
更新時間:2020/5/31 22:50,更新了基於數據庫的認證與授權
更新時間:2020/6/6 17:45,更新了SpringSecurity核心組件
近期要用到spring security這個框架,由於spring security是之前學的,而且當時也沒有深入的學習,對於該框架的用法有點陌生了,現重新學習spring security並在此做好筆記,本文會持續更新,不斷地擴充
本文僅爲記錄學習軌跡,如有侵權,聯繫刪除
一、Spring Security簡介
Spring Security 是 Spring 家族中的一個安全管理框架,主要用於 Spring 項目組中提供安全認證服務,該框架主要的核心功能有認證、授權和攻擊防護。
二、Spring Security入門系列
(1)默認登錄與註銷
文件名:springboot_security2
pom配置
<dependencies>
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!--SpringSecurity框架整合-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- thymeleaf和springsecurity5的整合 -->
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
</dependencies>
經過測試,發現一般只要配置了SpringSecurity之後,即pom導入配置後,只要一訪問控制器的接口,都會被攔截,自動跳轉到SpringSecurity自定義的登錄界面,界面如下:
Security自定義的賬號是user,密碼則是由控制檯生成
輸入賬號和密碼即可登錄成功,並跳轉到一開始輸入要訪問的頁面
在url後面輸入logout即可退出登錄,logout接口也是Security自己內部的接口。
後面真正使用的時候會自己重寫配置,配置自己寫的登錄頁面,以及做一些用戶權限處理。
(2)自定義表單登錄
文件名:springboot_security2
可以看到如果配置了SpringSecurity,Security會有自己的登錄頁面,並且會攔截任何頁面,Security會有自己的內部接口login和logout。當然,很多時候是不會用它自己內部的登陸頁面,更多的是用自己自定義的登錄頁面,用自定義的表單,只需要自己做一下配置即可。
首先自己新建一個配置類,並且繼承WebSecurityConfigurerAdapter,這是官方要求的,官方說明如下:
/**
* Provides a convenient base class for creating a {@link WebSecurityConfigurer}
* instance. The implementation allows customization by overriding methods.
*
* <p>
* Will automatically apply the result of looking up
* {@link AbstractHttpConfigurer} from {@link SpringFactoriesLoader} to allow
* developers to extend the defaults.
* To do this, you must create a class that extends AbstractHttpConfigurer and then create a file in the classpath at "META-INF/spring.factories" that looks something like:
* </p>
* <pre>
* org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer
* </pre>
* If you have multiple classes that should be added you can use "," to separate the values. For example:
*
* <pre>
* org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer = sample.MyClassThatExtendsAbstractHttpConfigurer, sample.OtherThatExtendsAbstractHttpConfigurer
* </pre>
*
*/
意思是說 WebSecurityConfigurerAdapter 提供了一種便利的方式去創建 WebSecurityConfigurer的實例,只需要重寫 WebSecurityConfigurerAdapter 的方法,即可配置攔截什麼URL、設置什麼權限等安全控制。
創建配置類
package com.zsc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Objects;
/**
* 沒有添加改配置,頁面會強制跳轉到springsecurity自己的登錄頁面
* 參考鏈接:https://www.cnblogs.com/dw3306/p/12751373.html
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 指定密碼的加密方式,不然定義認證規則那裏會報錯
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return Objects.equals(charSequence.toString(), s);
}
};
}
//配置忽略掉的 URL 地址,一般用於js,css,圖片等靜態資源
@Override
public void configure(WebSecurity web) throws Exception {
//web.ignoring() 用來配置忽略掉的 URL 地址,一般用於靜態文件
web.ignoring().antMatchers("/js/**", "/css/**","/fonts/**","/images/**","/lib/**");
}
// (認證)配置用戶及其對應的角色
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//數據在內存中定義,一般要去數據庫取,jdbc中去拿,
/**
* 懶羊羊,灰太狼,喜羊羊,小灰灰分別具有vip0,vip1,vip2,vip3的權限
* root則同時又vip0到vip3的所有權限
*/
//Spring security 5.0中新增了多種加密方式,也改變了密碼的格式。
//要想我們的項目還能夠正常登陸,需要修改一下configure中的代碼。我們要將前端傳過來的密碼進行某種方式加密
//spring security 官方推薦的是使用bcrypt加密方式。
auth.inMemoryAuthentication()
.withUser("懶羊羊").password("123").roles("vip0")
.and()
.withUser("灰太狼").password("123").roles("vip1")
.and()
.withUser("喜羊羊").password("123").roles("vip2")
.and()
.withUser("小灰灰").password("123").roles("vip3")
.and()
.withUser("root").password("123").roles("vip1","vip2","vip3");
}
// (授權)配置 URL 訪問權限,對應用戶的權限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();//開啓運行iframe嵌套頁面
//任何請求都必須經過身份驗證
http.authorizeRequests()
.anyRequest().authenticated();//任何請求都必須經過身份驗證
//開啓表單驗證
http.formLogin()
.and()
.formLogin()//開啓表單驗證
.loginPage("/toLogin")//跳轉到自定義的登錄頁面
.usernameParameter("name")//自定義表單的用戶名的name,默認爲username
.passwordParameter("pwd")//自定義表單的密碼的name,默認爲password
.loginProcessingUrl("/doLogin")//表單請求的地址,一般與form的action屬性一致,注意:不用自己寫doLogin接口,只要與form的action屬性一致即可
.successForwardUrl("/index")//登錄成功後跳轉的頁面(重定向)
.failureForwardUrl("/toLogin")//登錄失敗後跳轉的頁面(重定向)
.and()
.logout()//開啓註銷功能
.logoutSuccessUrl("/toLogin")//註銷後跳轉到哪一個頁面
.logoutUrl("/logout") // 配置註銷登錄請求URL爲"/logout"(默認也就是 /logout)
.clearAuthentication(true) // 清除身份認證信息
.invalidateHttpSession(true) //使Http會話無效
.permitAll() // 允許訪問登錄表單、登錄接口
.and().csrf().disable(); // 關閉csrf
}
}
這裏配置了幾個用戶懶羊羊,灰太狼,喜羊羊,小灰灰等用於登錄,一般這些用戶要從數據庫獲取,另外這裏給他們設置了對應的權限,vip0到vip3的權限,都是自己自定義的權限,主要是爲了下一節做授權操作(這裏還沒做授權操作)
除此之外,這裏有一個自己之前一直搞錯的重點如下
運行並訪問主頁index,會被攔截並且跳到自定義表單
隨便輸入自己上面定義好的用戶,跳轉到首頁,並且所有的頁面都可以訪問,vip0到vip3對應所有頁面均可以訪問
註銷登錄(退出)
以上就完成了自定義表單的登錄與註銷,下面開始做用戶授權。
(3)自定義表單用戶授權
文件名:springboot_security2
用戶授權簡單理解就是什麼用戶可以訪問什麼頁面,不同的用戶可以訪問不同的頁面,上一節已經給不同的用戶設置了的權限,下面給不同用戶做授權,配置類如下
package com.zsc.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Objects;
/**
* 沒有添加改配置,頁面會強制跳轉到springsecurity自己的登錄頁面
* 參考鏈接:https://www.cnblogs.com/dw3306/p/12751373.html
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 指定密碼的加密方式,不然定義認證規則那裏會報錯
@Bean
public PasswordEncoder passwordEncoder() {
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return Objects.equals(charSequence.toString(), s);
}
};
}
//配置忽略掉的 URL 地址,一般用於js,css,圖片等靜態資源
@Override
public void configure(WebSecurity web) throws Exception {
//web.ignoring() 用來配置忽略掉的 URL 地址,一般用於靜態文件
web.ignoring().antMatchers("/js/**", "/css/**","/fonts/**","/images/**","/lib/**");
}
// (認證)配置用戶及其對應的角色
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//數據在內存中定義,一般要去數據庫取,jdbc中去拿,
/**
* 懶羊羊,灰太狼,喜羊羊,小灰灰分別具有vip0,vip1,vip2,vip3的權限
* root則同時又vip0到vip3的所有權限
*/
//Spring security 5.0中新增了多種加密方式,也改變了密碼的格式。
//要想我們的項目還能夠正常登陸,需要修改一下configure中的代碼。我們要將前端傳過來的密碼進行某種方式加密
//spring security 官方推薦的是使用bcrypt加密方式。
auth.inMemoryAuthentication()
.withUser("懶羊羊").password("123").roles("vip0")
.and()
.withUser("灰太狼").password("123").roles("vip1")
.and()
.withUser("喜羊羊").password("123").roles("vip2")
.and()
.withUser("小灰灰").password("123").roles("vip3")
.and()
.withUser("root").password("123").roles("vip1","vip2","vip3");
}
// (授權)配置 URL 訪問權限,對應用戶的權限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();//開啓運行iframe嵌套頁面
//任何請求都必須經過身份驗證
http.authorizeRequests()
// .anyRequest().authenticated()//任何請求都必須經過身份驗證
.antMatchers("/vip/vip0/**").hasRole("vip0")//vip1具有的權限:只有vip1用戶纔可以訪問包含url路徑"/vip/vip0/**"
.antMatchers("/vip/vip1/**").hasRole("vip1")//vip1具有的權限:只有vip1用戶纔可以訪問包含url路徑"/vip/vip1/**"
.antMatchers("/vip/vip2/**").hasRole("vip2")//vip2具有的權限:只有vip2用戶纔可以訪問url路徑"/vip/vip2/**"
.antMatchers("/vip/vip3/**").hasRole("vip3");//vip3具有的權限:只有vip3用戶纔可以訪問url路徑"/vip/vip3/**"
//開啓表單驗證
http.formLogin()
.and()
.formLogin()//開啓表單驗證
.loginPage("/toLogin")//跳轉到自定義的登錄頁面
.usernameParameter("name")//自定義表單的用戶名的name,默認爲username
.passwordParameter("pwd")//自定義表單的密碼的name,默認爲password
.loginProcessingUrl("/doLogin")//表單請求的地址,一般與form的action屬性一致,注意:不用自己寫doLogin接口,只要與form的action屬性一致即可
.successForwardUrl("/index")//登錄成功後跳轉的頁面(重定向)
.failureForwardUrl("/toLogin")//登錄失敗後跳轉的頁面(重定向)
.and()
.logout()//開啓註銷功能
.logoutSuccessUrl("/toLogin")//註銷後跳轉到哪一個頁面
.logoutUrl("/logout") // 配置註銷登錄請求URL爲"/logout"(默認也就是 /logout)
.clearAuthentication(true) // 清除身份認證信息
.invalidateHttpSession(true) //使Http會話無效
.permitAll() // 允許訪問登錄表單、登錄接口
.and().csrf().disable(); // 關閉csrf
}
}
主要增加了用戶權限的配置,具體如下圖
不需要登錄直接進首頁,因爲沒對首頁index做限制,但是點擊vip0到vip3的任意頁面都會被攔截,並且自動跳轉到登錄頁面進行登錄
點擊vip0下對應的頁面、
灰太狼賬號登錄後,可以訪問vip1權限的頁面,其餘權限的頁面不可以訪問,如果訪問會拋出異常,因爲沒做相應異常的處理,所以異常會顯示在頁面
登錄具有不同權限的用戶,可以訪問對應權限的頁面,以上就是用戶授權的最基本的用戶。
(4)基於數據庫的自定義表單認證
文件名:springboot_security3
首先是數據庫的創建,實際上登錄認證至少要有5張表,用戶表、角色表、權限表,角色權限中間表和用戶角色中間表,這裏按照上面的例子,將權限直接寫死在自定義的SecurityConfig配置類中。
所以這裏的登錄認證只涉及到三張表:用戶表(user)、角色表(role)、用戶角色中間表(user_role)。
/*
Navicat Premium Data Transfer
Source Server : test3
Source Server Type : MySQL
Source Server Version : 80015
Source Host : localhost:3306
Source Schema : test2
Target Server Type : MySQL
Target Server Version : 80015
File Encoding : 65001
Date: 31/05/2020 22:01:56
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_vip0');
INSERT INTO `role` VALUES (2, 'ROLE_vip1');
INSERT INTO `role` VALUES (3, 'ROLE_vip2');
INSERT INTO `role` VALUES (4, 'ROLE_vip3');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'root', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq');
INSERT INTO `user` VALUES (3, '灰太狼', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq');
INSERT INTO `user` VALUES (4, '喜羊羊', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq');
INSERT INTO `user` VALUES (5, '懶羊羊', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq');
INSERT INTO `user` VALUES (6, '小灰灰', '$2a$10$RMuFXGQ5AtH4wOvkUqyvuecpqUSeoxZYqilXzbz50dceRsga.WYiq');
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`uid` int(11) NULL DEFAULT NULL,
`rid` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 5 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 1, 2);
INSERT INTO `user_role` VALUES (3, 1, 3);
INSERT INTO `user_role` VALUES (4, 1, 4);
INSERT INTO `user_role` VALUES (5, 3, 2);
INSERT INTO `user_role` VALUES (6, 4, 3);
INSERT INTO `user_role` VALUES (7, 6, 4);
INSERT INTO `user_role` VALUES (8, 5, 1);
SET FOREIGN_KEY_CHECKS = 1;
具體數據表截圖
注意:這裏的role跟上面的例子相比多加了ROLE_前綴。這是因爲之前的role都是通過springsecurity的api賦值過去的,他會自行幫我們加上這個前綴。但是現在我們使用的是自己的數據庫裏面讀取出來的權限,然後封裝到自己的實體類中。所以這時候需要我們自己手動添加這個ROLE_前綴。經過測試如果不加ROLE_前綴的話,可以做數據庫的認證,但無法做授權
創建實體類User,注意User需要實現UserDetails接口,並且實現該接口下的7個接口
package com.zsc.po;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements UserDetails {
private Integer id;
private String userName;//用戶名
private String passWord;//密碼
private List<Role> roles;//該用戶對應的角色
/**
* 返回用戶的權限集合。
* @return
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
/**
* 返回賬號的密碼
* @return
*/
@Override
public String getPassword() {
return passWord;
}
/**
* 返回賬號的用戶名
* @return
*/
@Override
public String getUsername() {
return userName;
}
/**
* 賬號是否失效,true:賬號有效,false賬號失效。
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 賬號是否被鎖,true:賬號沒被鎖,可用;false:賬號被鎖,不可用
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 賬號認證是否過期,true:沒過期,可用;false:過期,不可用
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 賬號是否可用,true:可用,false:不可用
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
}
角色表實體類Role,這個類不用實現上述接口
package com.zsc.po;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Role {
private Integer id;
private String name;//角色的名字
}
接下來做數據庫的查詢,創建持久層接口(UserMapper和RoleMapper)
package com.zsc.mapper;
import com.zsc.po.User;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
@Mapper
@Repository
public interface UserMapper {
/**
* 通過用戶名獲取用戶信息
*
* @param username 用戶名
* @return User 用戶信息
*/
List<User> getUserByUsername(String username);
}
package com.zsc.mapper;
import com.zsc.po.Role;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
import java.util.List;
@Mapper
@Repository
public interface RoleMapper {
/**
* 通過用戶id獲取用戶角色集合
*
* @param userId 用戶id
* @return List<Role> 角色集合
*/
List<Role> getRolesByUserId(Integer userId);
}
持久層接口對應配置文件(UserMapper.xml和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.zsc.mapper.UserMapper">
<resultMap id = "userMap" type = "com.zsc.po.User">
<id column="id" property="id"></id>
<result column="username" property="userName"></result>
<result column="password" property="passWord"></result>
<collection property="roles" ofType="com.zsc.po.Role">
<id property="id" column="rid"></id>
<result column="rname" property="name"></result>
</collection>
</resultMap>
<select id="getUserByUsername" resultMap="userMap">
select * from user where username = #{username}
</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.zsc.mapper.RoleMapper">
<resultMap id = "roleMap" type = "com.zsc.po.Role">
<id column="id" property="id"></id>
<result column="name" property="name"></result>
</resultMap>
<select id="getRolesByUserId" resultMap="roleMap">
select * from role r,user_role ur where r.id = ur.rid and ur.uid = #{userId}
</select>
</mapper>
創建服務層(UserService),該層獲取數據庫數據,將數據交給SpringSecurity做用戶的認證與授權,爲此,需要實現接口UserDetailsService,並且實現該接口下的loadUserByUsername方法,該方法獲取數據庫數據
package com.zsc.service;
import com.zsc.mapper.RoleMapper;
import com.zsc.mapper.UserMapper;
import com.zsc.po.User;
import org.springframework.beans.factory.annotation.Autowired;
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;
import java.util.List;
@Service
public class UserService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<User> users = userMapper.getUserByUsername(s);
if (null == users || users.size() ==0) {
throw new UsernameNotFoundException("該用戶不存在!");
}else{
users.get(0).setRoles(roleMapper.getRolesByUserId(users.get(0).getId()));
System.out.println("***********************"+users.get(0).getAuthorities());
return users.get(0);
}
}
}
最後修改一下自定義的SecurityConfig配置類即可
package com.zsc.config;
import com.zsc.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* 沒有添加改配置,頁面會強制跳轉到springsecurity自己的登錄頁面
* 參考鏈接:https://www.cnblogs.com/dw3306/p/12751373.html
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
//配置忽略掉的 URL 地址,一般用於js,css,圖片等靜態資源
@Override
public void configure(WebSecurity web) throws Exception {
//web.ignoring() 用來配置忽略掉的 URL 地址,一般用於靜態文件
web.ignoring().antMatchers("/js/**", "/css/**","/fonts/**","/images/**","/lib/**");
}
// (認證)配置用戶及其對應的角色
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
}
// (授權)配置 URL 訪問權限,對應用戶的權限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();//開啓運行iframe嵌套頁面
//身份驗證
http.authorizeRequests()
.anyRequest().authenticated();//任何請求都必須經過身份驗證
//開啓表單驗證
http.formLogin()
.and()
.formLogin()//開啓表單驗證
.loginPage("/toLogin")//跳轉到自定義的登錄頁面
.usernameParameter("name")//自定義表單的用戶名的name,默認爲username
.passwordParameter("pwd")//自定義表單的密碼的name,默認爲password
.loginProcessingUrl("/doLogin")//表單請求的地址,一般與form的action屬性一致,注意:不用自己寫doLogin接口,只要與form的action屬性一致即可
.successForwardUrl("/index")//登錄成功後跳轉的頁面(重定向)
.failureForwardUrl("/toLogin")//登錄失敗後跳轉的頁面(重定向)
.and()
.logout()//開啓註銷功能
.logoutSuccessUrl("/toLogin")//註銷後跳轉到哪一個頁面
.logoutUrl("/logout") // 配置註銷登錄請求URL爲"/logout"(默認也就是 /logout)
.clearAuthentication(true) // 清除身份認證信息
.invalidateHttpSession(true) //使Http會話無效
.permitAll() // 允許訪問登錄表單、登錄接口
.and().csrf().disable(); // 關閉csrf
}
}
大功告成,運行測試,訪問首頁index,被攔截重定向到登錄頁面進行用戶認證,即登錄認證
隨便輸入數據庫中存在的任一用戶,密碼是123,數據庫存儲的密碼是經過加密的,登錄成功,因爲沒做任何用戶的授權,所以可訪問任意頁面
這裏再重點記錄一下,關於數據庫存儲用戶權限必須要有ROLE_前綴,但在SecurityConfig中設置權限時可以不用加ROLE_前綴
這其實是授權部分的內容,下一節就是數據庫用戶的授權操作
(5)基於數據庫的自定義表單授權
文件名:springboot_security3
關於授權部分,上一節其實已經有講到一點了,實現起來頁簡單,基本的配置跟上一節一樣保持不變,唯一變的就是將攔截所有請求改爲對應權限的攔截,具體只要修改SecurityConfig配置類的部分內容即可
package com.zsc.config;
import com.zsc.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* 沒有添加改配置,頁面會強制跳轉到springsecurity自己的登錄頁面
* 參考鏈接:https://www.cnblogs.com/dw3306/p/12751373.html
*/
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserService userService;
//配置忽略掉的 URL 地址,一般用於js,css,圖片等靜態資源
@Override
public void configure(WebSecurity web) throws Exception {
//web.ignoring() 用來配置忽略掉的 URL 地址,一般用於靜態文件
web.ignoring().antMatchers("/js/**", "/css/**","/fonts/**","/images/**","/lib/**");
}
// (認證)配置用戶及其對應的角色
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
}
// (授權)配置 URL 訪問權限,對應用戶的權限
@Override
protected void configure(HttpSecurity http) throws Exception {
http.headers().frameOptions().disable();//開啓運行iframe嵌套頁面
//身份驗證
http.authorizeRequests()
// .anyRequest().authenticated();//任何請求都必須經過身份驗證
.antMatchers("/vip/vip0/**").hasRole("vip0")//vip1具有的權限:只有vip1用戶纔可以訪問包含url路徑"/vip/vip0/**"
.antMatchers("/vip/vip1/**").hasRole("vip1")//vip1具有的權限:只有vip1用戶纔可以訪問包含url路徑"/vip/vip1/**"
.antMatchers("/vip/vip2/**").hasRole("vip2")//vip2具有的權限:只有vip2用戶纔可以訪問url路徑"/vip/vip2/**"
.antMatchers("/vip/vip3/**").hasRole("vip3");//vip3具有的權限:只有vip3用戶纔可以訪問url路徑"/vip/vip3/**"
//開啓表單驗證
http.formLogin()
.and()
.formLogin()//開啓表單驗證
.loginPage("/toLogin")//跳轉到自定義的登錄頁面
.usernameParameter("name")//自定義表單的用戶名的name,默認爲username
.passwordParameter("pwd")//自定義表單的密碼的name,默認爲password
.loginProcessingUrl("/doLogin")//表單請求的地址,一般與form的action屬性一致,注意:不用自己寫doLogin接口,只要與form的action屬性一致即可
.successForwardUrl("/index")//登錄成功後跳轉的頁面(重定向)
.failureForwardUrl("/toLogin")//登錄失敗後跳轉的頁面(重定向)
.and()
.logout()//開啓註銷功能
.logoutSuccessUrl("/toLogin")//註銷後跳轉到哪一個頁面
.logoutUrl("/logout") // 配置註銷登錄請求URL爲"/logout"(默認也就是 /logout)
.clearAuthentication(true) // 清除身份認證信息
.invalidateHttpSession(true) //使Http會話無效
.permitAll() // 允許訪問登錄表單、登錄接口
.and().csrf().disable(); // 關閉csrf
}
}
這樣就完成了數據庫用戶的授權,測試運行,訪問主頁,跟之前一樣任何人可以登錄,因爲沒有對主頁做限制,但是訪問主頁裏面的vip0到vip3任意頁面都需要相應的權限,如果沒有會跳到登錄頁面進行登錄認證
(6)獲取當前登錄用戶的信息
登錄授權後,很多時候都需要用戶登錄的用戶的基本信息,比如判斷用戶是在線,獲取當前登錄用戶的關聯的信息等。都需要用到當前登錄用戶的信息,下面是獲取當前登錄用戶信息的一種方法,主要是在控制層獲取。
@GetMapping("/isLogin")
@ResponseBody
public Object getUserInfo(){
if(!SecurityContextHolder.getContext().getAuthentication().getName().equals("anonymousUser")){
//已登錄
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();//獲取用戶信息
//獲取登錄的用戶名
String username = authentication.getName();
System.out.println("username : "+username);
//用戶的所有權限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
System.out.println("authorities : "+authorities);
/**
* 如果要獲取更詳細的用戶信息可以採用下面這種方法
*/
//用戶的基本信息
User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
System.out.println("user : "+user);
//用戶的id
Integer userId = user.getId();
System.out.println("userId: "+userId);
//User其餘信息可以用這種方式獲取
//List<Role> roles = user.getRoles();
//String password = user.getPassword();
//String username1 = user.getUsername();
return "已登錄賬號:"+username;
}else{
//未登錄
return "請先登錄";
}
在沒有登錄的狀態下訪問上面的接口
登錄灰太狼賬號之後,查看頁面,同時觀察控制檯輸出
以上就是獲取當前登錄賬號的個人信息的全部內容。
三、SpringSecurity核心組件
這裏列舉出以下核心組件:SecurityContext、SecurityContextHolder、Authentication、Userdetails 和 AuthenticationManager,下面開始對這些核心組件的詳細介紹。
(1)Authentication
authentication 直譯過來是“認證”的意思,在Spring Security 中Authentication用來表示當前用戶是誰,一般來講你可以理解爲authentication就是一組用戶名密碼信息。Authentication也是一個接口,其定義如下:
我們獲取當前登錄用戶信息就是用的這個接口,如果有看上面入門系列的獲取用戶那一段,就可以知道獲取用戶信息也就是用的Authentication
(2)SecurityContext
安全上下文,用戶通過Spring Security 的校驗之後,驗證信息存儲在SecurityContext中,SecurityContext的接口定義如下:
可以看到這裏只定義了兩個方法,主要都是用來獲取或修改認證信息(Authentication)的,Authentication是用來存儲着認證用戶的信息,所以這個接口可以間接獲取到用戶的認證信息。還是以上面的入門系列的獲取用戶那一段來進行解析
(3)SecurityContextHolder
SecurityContextHolder看名字就知道跟SecurityContext實例相關的。在典型的web應用程序中,用戶登錄一次,然後由其會話ID標識。服務器緩存持續時間會話的主體信息。
但是在Spring Security中,在請求之間存儲SecurityContext的責任落在SecurityContextPersistenceFilter上,默認情況下,該上下文將上下文存儲爲HTTP請求之間的HttpSession屬性。它會爲每個請求恢復上下文SecurityContextHolder,並且最重要的是,在請求完成時清除SecurityContextHolder
說到SecurityContextHolder就必須要說到一個過濾器,SecurityContextPersistenceFilter
SecurityContextPersistenceFilter:這個Filter是整個攔截過程的入口和出口 ,在請求開始時從配置好的SecurityContextRepository中獲取SecurityContext,然後把它設置給 SecurityContextHolder。在請求完成後將 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同時清除 SecurityContextHolder 所持有的SecurityContext ;
進入源碼查看
同樣的可以參考上面的入門系列的獲取用戶那一段來進行解析
可以看整個登錄用戶的信息獲取流程就十分清晰了。
(4)UserDetails
這個看着有點熟悉,在上面入門系列的基於數據庫認證中,實體類User就必須實現這個接口
UserDetails存儲的就是用戶信息,其定義如下:
(5)UserDetailsService
在上面入門系列的基於數據庫認證中,用戶類必須要實現UserDetails接口,還需要實現UserDetailsService接口,與實體類User(實現了UserDetails接口)
相對應的還要在應用層中實現UserDetailsService接口
之前在入門系列中的基於數據庫的認證中沒有去深究其原理,到現在基本就可以知道其認證的流程,包括數據庫用戶的獲取。
通常在spring security應用中,我們會自定義一個UserDetailsService來實現UserDetailsService接口,並實現其loadUserByUsername(final String login);方法。我們在實現loadUserByUsername方法的時候,就可以通過查詢數據庫(或者是緩存、或者是其他的存儲形式)來獲取用戶信息,然後組裝成一個UserDetails,(通常是一個org.springframework.security.core.userdetails.User,它繼承自UserDetails) 並返回。
在實現loadUserByUsername方法的時候,如果我們通過查庫沒有查到相關記錄,需要拋出一個異常來告訴spring security來“善後”。這個異常是org.springframework.security.core.userdetails.UsernameNotFoundException。
關於其源碼估計能猜到,肯定有一個loadUserByUsername方法等着我們去實現
通常基於數據庫的認證,就要從數據庫中獲取要認證的用戶信息,從數據庫中獲取用戶信息就是通過服務處實現UserDetailsService接口,並重寫其loadUserByUsername方法,這個方法用來獲取數據庫用戶信息。
(6)AuthenticationManager
AuthenticationManager 是一個接口,它只有一個方法,接收參數爲Authentication,其定義如下:
public interface AuthenticationManager {
Authentication authenticate(Authentication authentication)
throws AuthenticationException;
}
AuthenticationManager 的作用就是校驗Authentication,如果驗證失敗會拋出AuthenticationException異常。AuthenticationException是一個抽象類,因此代碼邏輯並不能實例化一個AuthenticationException異常並拋出,實際上拋出的異常通常是其實現類,如DisabledException,LockedException,BadCredentialsException等。BadCredentialsException可能會比較常見,即密碼錯誤的時候。
四、部分源碼解析
(1)用戶認證流程
認證大致流程
關於SpringSecurity的用戶認證流程,個人覺得是十分有必要了解的,儘管框架已經封裝好,只要按照它定好的規則來做就好了。在查詢了大量的博客和網上的大量視頻講解後,發現其實講的基本都一樣,當然有些自己還沒搞懂,個人覺得任何東西如果自己不動手試一下是不可能真正懂的。
首先是大致流程,之前我想的是它可能是通過過濾器的方式去實現的攔截並且重定向到登錄頁面的方式進行認證的,在查閱了大量的資料發現,實現的方式確實是過濾器的方式,只不過它有很多個過濾器,形成一條過濾鏈,只有通過這條過濾鏈後纔可以訪問API
具體的驗證流程可以用下圖來表示
下面介紹過濾器鏈中主要的幾個過濾器及其作用:
SecurityContextPersistenceFilter:這個Filter是整個攔截過程的入口和出口 ,在請求開始時從配置好的SecurityContextRepository中獲取SecurityContext,然後把它設置給 SecurityContextHolder。在請求完成後將 SecurityContextHolder 持有的 SecurityContext 再保存到配置好的 SecurityContextRepository,同時清除 SecurityContextHolder 所持有的SecurityContext ;
UsernamePasswordAuthenticationFilter:用於處理來自表單提交的認證。該表單必須提供對應的用戶名和密碼,其內部還有登錄成功或失敗後進行處理的AuthenticationSuccessHandler和 AuthenticationFailureHandler,這兩個接口可以字配置,在上面入門系列的自定義的SecurityConfig配置類中可以自己配置
FilterSecuritylnterceptor:是用於保護web資源的,使用AccessDecisionManager對當前用戶進行授權訪問
ExceptionTranslationFilter:捕獲來自FilterChain所有的異常並進行處理。但是它只會處理兩類異 常:Authentication Exception 和 AccessDeniedException ,其它的異常它會繼續拋出。
認證具體流程
這裏推薦一篇個人覺得簡單易懂認證流程的博客:https://www.cnblogs.com/ymstars/p/10626786.html
具體的認證流程需要看源碼才能知道,這裏引用一下之前看的視頻的一張認證的圖片,圖片如下
從這裏看到請求進來會經過UsernamePasswordAuthenticationFilter 過濾器,所以先用全局搜索(CTRL+N)找到該過濾器,並且打上斷點,這裏用的默認登錄頁面,密碼用的控制檯隨機生成的密碼
就像上面認證大致流程裏面說的一樣,用戶身份的認證交給AuthenticationManager處理,AuthenticationManager又委託給DaoAuthenticationProvider 認證,所以在全局搜索找到DaoAuthenticationProvider並且打上斷點進行調試
注意這行代碼:
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
它是通過UserDetailsService來加載要驗證的用戶
獲取用戶名後,將用戶名傳給preAuthenticationChecks.check()方法驗證
進入preAuthenticationChecks.check(user);內部方法,可以看到有一些驗證,如賬號是否可用,是否被鎖等等,這些參數就是上面數據庫用戶認證User類要認證的參數
用戶賬號密碼的驗證則是由斷點下面的additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);負責驗證,進入該方法
總結:
(1)UsernamePasswordAuthenticationFilter獲取表單輸入的用戶名和密碼等請求信息,並封裝成Token
(2)AuthenticationManager負責將Token委託給DaoAuthenticationProvider進行認證
(3)DaoAuthenticationProvider通過UserDetailsService來加載要驗證的用戶
(4)最後先校驗用戶的賬號是否被鎖了等信息,再校驗用戶賬號和密碼
(5)校驗成功則可以訪問接口,失敗則拋出異常
以上就是用戶認證具體流程的全部內容,涉及到一些源碼解讀,有點累人,剛開始以爲很複雜,但自己試着調試了一下,基本還是可以理解的。
(2)默認登錄用戶名與密碼配置
如果成功引入Security依賴,MVC Security安全管理功能就行會自動生效,默認的安全配置是在UserDetailsServiceAutoConfiguration和SecurityAutoConfiguration中實現的,其中SecurityAutoConfiguration會導入並且自動配置,SpringBootWebSecurityConfiguration用於啓動Web安全管理,UserDetailsServiceAutoConfiguration用於配置用戶信息。
關於Security內部配置的用戶名和密碼可以進入源碼查看它的配置,讀源碼真的是一種難受的事情。