詳解Spring Boot Security工作流程
簡介
Spring Security,這是一種基于 Spring AOP 和 Servlet 過濾器的安全框架。它提供全面的安全性解決方案,同時在 Web 請求級和方法調(diào)用級處理身份確認(rèn)和授權(quán)。
工作流程
從網(wǎng)上找了一張Spring Security 的工作流程圖,如下。

圖中標(biāo)記的MyXXX,就是我們項目中需要配置的。
快速上手
建表
表結(jié)構(gòu)

建表語句
DROP TABLE IF EXISTS `user`; DROP TABLE IF EXISTS `role`; DROP TABLE IF EXISTS `user_role`; DROP TABLE IF EXISTS `role_permission`; DROP TABLE IF EXISTS `permission`; CREATE TABLE `user` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `role` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `user_role` ( `user_id` bigint(11) NOT NULL, `role_id` bigint(11) NOT NULL ); CREATE TABLE `role_permission` ( `role_id` bigint(11) NOT NULL, `permission_id` bigint(11) NOT NULL ); CREATE TABLE `permission` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `url` varchar(255) NOT NULL, `name` varchar(255) NOT NULL, `description` varchar(255) NULL, `pid` bigint(11) NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO user (id, username, password) VALUES (1,'user','e10adc3949ba59abbe56e057f20f883e'); INSERT INTO user (id, username , password) VALUES (2,'admin','e10adc3949ba59abbe56e057f20f883e'); INSERT INTO role (id, name) VALUES (1,'USER'); INSERT INTO role (id, name) VALUES (2,'ADMIN'); INSERT INTO permission (id, url, name, pid) VALUES (1,'/user/common','common',0); INSERT INTO permission (id, url, name, pid) VALUES (2,'/user/admin','admin',0); INSERT INTO user_role (user_id, role_id) VALUES (1, 1); INSERT INTO user_role (user_id, role_id) VALUES (2, 1); INSERT INTO user_role (user_id, role_id) VALUES (2, 2); INSERT INTO role_permission (role_id, permission_id) VALUES (1, 1); INSERT INTO role_permission (role_id, permission_id) VALUES (2, 1); INSERT INTO role_permission (role_id, permission_id) VALUES (2, 2);
pom.xml
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <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.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-security4</artifactId> </dependency>
User
public class User implements UserDetails , Serializable {
private Long id;
private String username;
private String password;
private List<Role> authorities;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public List<Role> getAuthorities() {
return authorities;
}
public void setAuthorities(List<Role> authorities) {
this.authorities = authorities;
}
/**
* 用戶賬號是否過期
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 用戶賬號是否被鎖定
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 用戶密碼是否過期
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 用戶是否可用
*/
@Override
public boolean isEnabled() {
return true;
}
}上面的 User 類實現(xiàn)了 UserDetails 接口,該接口是實現(xiàn)Spring Security 認(rèn)證信息的核心接口。其中 getUsername 方法為 UserDetails 接口 的方法,這個方法返回 username,也可以是其他的用戶信息,例如手機(jī)號、郵箱等。getAuthorities() 方法返回的是該用戶設(shè)置的權(quán)限信息,在本實例中,從數(shù)據(jù)庫取出用戶的所有角色信息,權(quán)限信息也可以是用戶的其他信息,不一定是角色信息。另外需要讀取密碼,最后幾個方法一般情況下都返回 true,也可以根據(jù)自己的需求進(jìn)行業(yè)務(wù)判斷。
Role
public class Role implements GrantedAuthority {
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String getAuthority() {
return name;
}
}Role 類實現(xiàn)了 GrantedAuthority 接口,并重寫 getAuthority() 方法。權(quán)限點可以為任何字符串,不一定是非要用角色名。
所有的Authentication實現(xiàn)類都保存了一個GrantedAuthority列表,其表示用戶所具有的權(quán)限。GrantedAuthority是通過AuthenticationManager設(shè)置到Authentication對象中的,然后AccessDecisionManager將從Authentication中獲取用戶所具有的GrantedAuthority來鑒定用戶是否具有訪問對應(yīng)資源的權(quán)限。
MyUserDetailsService
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private RoleMapper roleMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
//查數(shù)據(jù)庫
User user = userMapper.loadUserByUsername( userName );
if (null != user) {
List<Role> roles = roleMapper.getRolesByUserId( user.getId() );
user.setAuthorities( roles );
}
return user;
}
}Service 層需要實現(xiàn) UserDetailsService 接口,該接口是根據(jù)用戶名獲取該用戶的所有信息, 包括用戶信息和權(quán)限點。
MyInvocationSecurityMetadataSourceService
@Component
public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource {
@Autowired
private PermissionMapper permissionMapper;
/**
* 每一個資源所需要的角色 Collection<ConfigAttribute>決策器會用到
*/
private static HashMap<String, Collection<ConfigAttribute>> map =null;
/**
* 返回請求的資源需要的角色
*/
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
if (null == map) {
loadResourceDefine();
}
//object 中包含用戶請求的request 信息
HttpServletRequest request = ((FilterInvocation) o).getHttpRequest();
for (Iterator<String> it = map.keySet().iterator() ; it.hasNext();) {
String url = it.next();
if (new AntPathRequestMatcher( url ).matches( request )) {
return map.get( url );
}
}
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
/**
* 初始化 所有資源 對應(yīng)的角色
*/
public void loadResourceDefine() {
map = new HashMap<>(16);
//權(quán)限資源 和 角色對應(yīng)的表 也就是 角色權(quán)限 中間表
List<RolePermisson> rolePermissons = permissionMapper.getRolePermissions();
//某個資源 可以被哪些角色訪問
for (RolePermisson rolePermisson : rolePermissons) {
String url = rolePermisson.getUrl();
String roleName = rolePermisson.getRoleName();
ConfigAttribute role = new SecurityConfig(roleName);
if(map.containsKey(url)){
map.get(url).add(role);
}else{
List<ConfigAttribute> list = new ArrayList<>();
list.add( role );
map.put( url , list );
}
}
}
}MyInvocationSecurityMetadataSourceService 類實現(xiàn)了 FilterInvocationSecurityMetadataSource,F(xiàn)ilterInvocationSecurityMetadataSource 的作用是用來儲存請求與權(quán)限的對應(yīng)關(guān)系。
FilterInvocationSecurityMetadataSource接口有3個方法:
boolean supports(Class<?> clazz):指示該類是否能夠為指定的方法調(diào)用或Web請求提供ConfigAttributes。
Collection
getAllConfigAttributes():Spring容器啟動時自動調(diào)用, 一般把所有請求與權(quán)限的對應(yīng)關(guān)系也要在這個方法里初始化, 保存在一個屬性變量里。
Collection
getAttributes(Object object):當(dāng)接收到一個http請求時, filterSecurityInterceptor會調(diào)用的方法. 參數(shù)object是一個包含url信息的HttpServletRequest實例. 這個方法要返回請求該url所需要的所有權(quán)限集合。
MyAccessDecisionManager
/**
* 決策器
*/
@Component
public class MyAccessDecisionManager implements AccessDecisionManager {
private final static Logger logger = LoggerFactory.getLogger(MyAccessDecisionManager.class);
/**
* 通過傳遞的參數(shù)來決定用戶是否有訪問對應(yīng)受保護(hù)對象的權(quán)限
*
* @param authentication 包含了當(dāng)前的用戶信息,包括擁有的權(quán)限。這里的權(quán)限來源就是前面登錄時UserDetailsService中設(shè)置的authorities。
* @param object 就是FilterInvocation對象,可以得到request等web資源
* @param configAttributes configAttributes是本次訪問需要的權(quán)限
*/
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
if (null == configAttributes || 0 >= configAttributes.size()) {
return;
} else {
String needRole;
for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) {
needRole = iter.next().getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) {
if(needRole.trim().equals(ga.getAuthority().trim())) {
return;
}
}
}
throw new AccessDeniedException("當(dāng)前訪問沒有權(quán)限");
}
}
/**
* 表示此AccessDecisionManager是否能夠處理傳遞的ConfigAttribute呈現(xiàn)的授權(quán)請求
*/
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
/**
* 表示當(dāng)前AccessDecisionManager實現(xiàn)是否能夠為指定的安全對象(方法調(diào)用或Web請求)提供訪問控制決策
*/
@Override
public boolean supports(Class<?> aClass) {
return true;
}}MyAccessDecisionManager 類實現(xiàn)了AccessDecisionManager接口,AccessDecisionManager是由AbstractSecurityInterceptor調(diào)用的,它負(fù)責(zé)鑒定用戶是否有訪問對應(yīng)資源(方法或URL)的權(quán)限。
MyFilterSecurityInterceptor
@Component
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {
@Autowired
private FilterInvocationSecurityMetadataSource securityMetadataSource;
@Autowired
public void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {
super.setAccessDecisionManager(myAccessDecisionManager);
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
FilterInvocation fi = new FilterInvocation(servletRequest, servletResponse, filterChain);
invoke(fi);
}
public void invoke(FilterInvocation fi) throws IOException, ServletException {
InterceptorStatusToken token = super.beforeInvocation(fi);
try {
//執(zhí)行下一個攔截器
fi.getChain().doFilter(fi.getRequest(), fi.getResponse());
} finally {
super.afterInvocation(token, null);
}
}
@Override
public Class<?> getSecureObjectClass() {
return FilterInvocation.class;
}
@Override
public SecurityMetadataSource obtainSecurityMetadataSource() {
return this.securityMetadataSource;
}
}每種受支持的安全對象類型(方法調(diào)用或Web請求)都有自己的攔截器類,它是AbstractSecurityInterceptor的子類,AbstractSecurityInterceptor 是一個實現(xiàn)了對受保護(hù)對象的訪問進(jìn)行攔截的抽象類。
AbstractSecurityInterceptor的機(jī)制可以分為幾個步驟:
1. 查找與當(dāng)前請求關(guān)聯(lián)的“配置屬性(簡單的理解就是權(quán)限)”
2. 將 安全對象(方法調(diào)用或Web請求)、當(dāng)前身份驗證、配置屬性 提交給決策器(AccessDecisionManager)
3. (可選)更改調(diào)用所根據(jù)的身份驗證
4. 允許繼續(xù)進(jìn)行安全對象調(diào)用(假設(shè)授予了訪問權(quán))
5. 在調(diào)用返回之后,如果配置了AfterInvocationManager。如果調(diào)用引發(fā)異常,則不會調(diào)用AfterInvocationManager。
AbstractSecurityInterceptor中的方法說明:
beforeInvocation()方法實現(xiàn)了對訪問受保護(hù)對象的權(quán)限校驗,內(nèi)部用到了AccessDecisionManager和AuthenticationManager;
finallyInvocation()方法用于實現(xiàn)受保護(hù)對象請求完畢后的一些清理工作,主要是如果在beforeInvocation()中改變了SecurityContext,則在finallyInvocation()中需要將其恢復(fù)為原來的SecurityContext,該方法的調(diào)用應(yīng)當(dāng)包含在子類請求受保護(hù)資源時的finally語句塊中。
afterInvocation()方法實現(xiàn)了對返回結(jié)果的處理,在注入了AfterInvocationManager的情況下默認(rèn)會調(diào)用其decide()方法。
了解了AbstractSecurityInterceptor,就應(yīng)該明白了,我們自定義MyFilterSecurityInterceptor就是想使用我們之前自定義的 AccessDecisionManager 和 securityMetadataSource。
SecurityConfig
@EnableWebSecurity注解以及WebSecurityConfigurerAdapter一起配合提供基于web的security。自定義類 繼承了WebSecurityConfigurerAdapter來重寫了一些方法來指定一些特定的Web安全設(shè)置。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService userService;
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
//校驗用戶
auth.userDetailsService( userService ).passwordEncoder( new PasswordEncoder() {
//對密碼進(jìn)行加密
@Override
public String encode(CharSequence charSequence) {
System.out.println(charSequence.toString());
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
//對密碼進(jìn)行判斷匹配
@Override
public boolean matches(CharSequence charSequence, String s) {
String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
boolean res = s.equals( encode );
return res;
}
} );
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/","index","/login","/login-error","/401","/css/**","/js/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().loginPage( "/login" ).failureUrl( "/login-error" )
.and()
.exceptionHandling().accessDeniedPage( "/401" );
http.logout().logoutSuccessUrl( "/" );
}
}MainController
@Controller
public class MainController {
@RequestMapping("/")
public String root() {
return "redirect:/index";
}
@RequestMapping("/index")
public String index() {
return "index";
}
@RequestMapping("/login")
public String login() {
return "login";
}
@RequestMapping("/login-error")
public String loginError(Model model) {
model.addAttribute( "loginError" , true);
return "login";
}
@GetMapping("/401")
public String accessDenied() {
return "401";
}
@GetMapping("/user/common")
public String common() {
return "user/common";
}
@GetMapping("/user/admin")
public String admin() {
return "user/admin";
}
}頁面
login.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<head>
<meta charset="UTF-8">
<title>首頁</title>
</head>
<body>
<h2>page list</h2>
<a href="/user/common" rel="external nofollow" rel="external nofollow" >common page</a>
<a href="/user/admin" rel="external nofollow" rel="external nofollow" >admin page</a>
<form th:action="@{/logout}" method="post">
<input type="submit" class="btn btn-primary" value="注銷"/>
</form>
</body>
</html>index.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<head>
<meta charset="UTF-8">
<title>首頁</title>
</head>
<body>
<h2>page list</h2>
<a href="/user/common" rel="external nofollow" rel="external nofollow" >common page</a>
<a href="/user/admin" rel="external nofollow" rel="external nofollow" >admin page</a>
<form th:action="@{/logout}" method="post">
<input type="submit" class="btn btn-primary" value="注銷"/>
</form>
</body>
</html>admin.html
<!DOCTYPE html> <head> <meta charset="UTF-8"> <title>admin page</title> </head> <body> success admin page?。?! </body> </html>
common.html
<!DOCTYPE html> <head> <meta charset="UTF-8"> <title>common page</title> </head> <body> success common page!?。? </body> </html>
401.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>401 page</title> </head> <body> <div> <div> <h2>權(quán)限不夠</h2> <p>拒絕訪問!</p> </div> </div> </body> </html>
最后運行項目,可以分別用 user、admin 賬號 去測試認(rèn)證和授權(quán)是否正確。
總結(jié)
以上所述是小編給大家介紹的Spring Boot Security,希望對大家有所幫助,如果大家有任何疑問請給我留言,小編會及時回復(fù)大家的。在此也非常感謝大家對腳本之家網(wǎng)站的支持!
- Spring Boot Security 結(jié)合 JWT 實現(xiàn)無狀態(tài)的分布式API接口
- SpringBoot+Spring Security+JWT實現(xiàn)RESTful Api權(quán)限控制的方法
- Spring Boot整合Spring Security簡單實現(xiàn)登入登出從零搭建教程
- Spring Boot2.0使用Spring Security的示例代碼
- SpringBoot + SpringSecurity 短信驗證碼登錄功能實現(xiàn)
- SpringBoot結(jié)合SpringSecurity實現(xiàn)圖形驗證碼功能
- Spring Boot Security配置教程
相關(guān)文章
Java 如何從list中刪除符合條件的數(shù)據(jù)
這篇文章主要介紹了Java 如何從list中刪除符合條件的數(shù)據(jù),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11
Spring中的注解@Autowired實現(xiàn)過程全解(@Autowired 背后的故事)
這篇文章主要介紹了Spring中的注解@Autowired實現(xiàn)過程全解,給大家聊聊@Autowired 背后的故事及實現(xiàn)原理,需要的朋友可以參考下2021-07-07
Java多線程中的ReentrantLock可中斷鎖詳細(xì)解讀
這篇文章主要介紹了Java多線程中的ReentrantLock可中斷鎖詳細(xì)解讀,ReentrantLock中的lockInterruptibly()方法使得線程可以在被阻塞時響應(yīng)中斷,比如一個線程t1通過lockInterruptibly()方法獲取到一個可重入鎖,并執(zhí)行一個長時間的任務(wù),需要的朋友可以參考下2023-12-12
SpringBoot使用Spring-Data-Jpa實現(xiàn)CRUD操作
這篇文章主要為大家詳細(xì)介紹了SpringBoot使用Spring-Data-Jpa實現(xiàn)CRUD操作,具有一定的參考價值,感興趣的小伙伴們可以參考一下2018-08-08
SpringBoot實現(xiàn)定時發(fā)送郵件的三種方法案例詳解
這篇文章主要介紹了SpringBoot三種方法實現(xiàn)定時發(fā)送郵件的案例,Spring框架的定時任務(wù)調(diào)度功能支持配置和注解兩種方式Spring?Boot在Spring框架的基礎(chǔ)上實現(xiàn)了繼承,并對其中基于注解方式的定時任務(wù)實現(xiàn)了非常好的支持,本文給大家詳細(xì)講解,需要的朋友可以參考下2023-03-03
SpringBoot+微信小程序?qū)崿F(xiàn)文件上傳與下載功能詳解
這篇文章主要為大家介紹了SpringBoot整合微信小程序?qū)崿F(xiàn)文件上傳與下載功能,文中的實現(xiàn)步驟講解詳細(xì),快跟隨小編一起學(xué)習(xí)一下吧2022-03-03

