詳解Spring Boot Security工作流程
簡(jiǎn)介
Spring Security,這是一種基于 Spring AOP 和 Servlet 過(guò)濾器的安全框架。它提供全面的安全性解決方案,同時(shí)在 Web 請(qǐng)求級(jí)和方法調(diào)用級(jí)處理身份確認(rèn)和授權(quán)。
工作流程
從網(wǎng)上找了一張Spring Security 的工作流程圖,如下。
圖中標(biāo)記的MyXXX,就是我們項(xiàng)目中需要配置的。
快速上手
建表
表結(jié)構(gòu)
建表語(yǔ)句
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; } /** * 用戶賬號(hào)是否過(guò)期 */ @Override public boolean isAccountNonExpired() { return true; } /** * 用戶賬號(hào)是否被鎖定 */ @Override public boolean isAccountNonLocked() { return true; } /** * 用戶密碼是否過(guò)期 */ @Override public boolean isCredentialsNonExpired() { return true; } /** * 用戶是否可用 */ @Override public boolean isEnabled() { return true; } }
上面的 User 類實(shí)現(xiàn)了 UserDetails 接口,該接口是實(shí)現(xiàn)Spring Security 認(rèn)證信息的核心接口。其中 getUsername 方法為 UserDetails 接口 的方法,這個(gè)方法返回 username,也可以是其他的用戶信息,例如手機(jī)號(hào)、郵箱等。getAuthorities() 方法返回的是該用戶設(shè)置的權(quán)限信息,在本實(shí)例中,從數(shù)據(jù)庫(kù)取出用戶的所有角色信息,權(quán)限信息也可以是用戶的其他信息,不一定是角色信息。另外需要讀取密碼,最后幾個(gè)方法一般情況下都返回 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 類實(shí)現(xiàn)了 GrantedAuthority 接口,并重寫 getAuthority() 方法。權(quán)限點(diǎn)可以為任何字符串,不一定是非要用角色名。
所有的Authentication實(shí)現(xiàn)類都保存了一個(gè)GrantedAuthority列表,其表示用戶所具有的權(quán)限。GrantedAuthority是通過(guò)AuthenticationManager設(shè)置到Authentication對(duì)象中的,然后AccessDecisionManager將從Authentication中獲取用戶所具有的GrantedAuthority來(lái)鑒定用戶是否具有訪問(wèn)對(duì)應(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ù)庫(kù) User user = userMapper.loadUserByUsername( userName ); if (null != user) { List<Role> roles = roleMapper.getRolesByUserId( user.getId() ); user.setAuthorities( roles ); } return user; } }
Service 層需要實(shí)現(xiàn) UserDetailsService 接口,該接口是根據(jù)用戶名獲取該用戶的所有信息, 包括用戶信息和權(quán)限點(diǎn)。
MyInvocationSecurityMetadataSourceService
@Component public class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource { @Autowired private PermissionMapper permissionMapper; /** * 每一個(gè)資源所需要的角色 Collection<ConfigAttribute>決策器會(huì)用到 */ private static HashMap<String, Collection<ConfigAttribute>> map =null; /** * 返回請(qǐng)求的資源需要的角色 */ @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { if (null == map) { loadResourceDefine(); } //object 中包含用戶請(qǐng)求的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; } /** * 初始化 所有資源 對(duì)應(yīng)的角色 */ public void loadResourceDefine() { map = new HashMap<>(16); //權(quán)限資源 和 角色對(duì)應(yīng)的表 也就是 角色權(quán)限 中間表 List<RolePermisson> rolePermissons = permissionMapper.getRolePermissions(); //某個(gè)資源 可以被哪些角色訪問(wèn) 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 類實(shí)現(xiàn)了 FilterInvocationSecurityMetadataSource,F(xiàn)ilterInvocationSecurityMetadataSource 的作用是用來(lái)儲(chǔ)存請(qǐng)求與權(quán)限的對(duì)應(yīng)關(guān)系。
FilterInvocationSecurityMetadataSource接口有3個(gè)方法:
boolean supports(Class<?> clazz):指示該類是否能夠?yàn)橹付ǖ姆椒ㄕ{(diào)用或Web請(qǐng)求提供ConfigAttributes。
Collection
getAllConfigAttributes():Spring容器啟動(dòng)時(shí)自動(dòng)調(diào)用, 一般把所有請(qǐng)求與權(quán)限的對(duì)應(yīng)關(guān)系也要在這個(gè)方法里初始化, 保存在一個(gè)屬性變量里。
Collection
getAttributes(Object object):當(dāng)接收到一個(gè)http請(qǐng)求時(shí), filterSecurityInterceptor會(huì)調(diào)用的方法. 參數(shù)object是一個(gè)包含url信息的HttpServletRequest實(shí)例. 這個(gè)方法要返回請(qǐng)求該url所需要的所有權(quán)限集合。
MyAccessDecisionManager
/** * 決策器 */ @Component public class MyAccessDecisionManager implements AccessDecisionManager { private final static Logger logger = LoggerFactory.getLogger(MyAccessDecisionManager.class); /** * 通過(guò)傳遞的參數(shù)來(lái)決定用戶是否有訪問(wèn)對(duì)應(yīng)受保護(hù)對(duì)象的權(quán)限 * * @param authentication 包含了當(dāng)前的用戶信息,包括擁有的權(quán)限。這里的權(quán)限來(lái)源就是前面登錄時(shí)UserDetailsService中設(shè)置的authorities。 * @param object 就是FilterInvocation對(duì)象,可以得到request等web資源 * @param configAttributes configAttributes是本次訪問(wèn)需要的權(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)前訪問(wèn)沒(méi)有權(quán)限"); } } /** * 表示此AccessDecisionManager是否能夠處理傳遞的ConfigAttribute呈現(xiàn)的授權(quán)請(qǐng)求 */ @Override public boolean supports(ConfigAttribute configAttribute) { return true; } /** * 表示當(dāng)前AccessDecisionManager實(shí)現(xiàn)是否能夠?yàn)橹付ǖ陌踩珜?duì)象(方法調(diào)用或Web請(qǐng)求)提供訪問(wèn)控制決策 */ @Override public boolean supports(Class<?> aClass) { return true; }}
MyAccessDecisionManager 類實(shí)現(xiàn)了AccessDecisionManager接口,AccessDecisionManager是由AbstractSecurityInterceptor調(diào)用的,它負(fù)責(zé)鑒定用戶是否有訪問(wèn)對(duì)應(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í)行下一個(gè)攔截器 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; } }
每種受支持的安全對(duì)象類型(方法調(diào)用或Web請(qǐng)求)都有自己的攔截器類,它是AbstractSecurityInterceptor的子類,AbstractSecurityInterceptor 是一個(gè)實(shí)現(xiàn)了對(duì)受保護(hù)對(duì)象的訪問(wèn)進(jìn)行攔截的抽象類。
AbstractSecurityInterceptor的機(jī)制可以分為幾個(gè)步驟:
1. 查找與當(dāng)前請(qǐng)求關(guān)聯(lián)的“配置屬性(簡(jiǎn)單的理解就是權(quán)限)”
2. 將 安全對(duì)象(方法調(diào)用或Web請(qǐng)求)、當(dāng)前身份驗(yàn)證、配置屬性 提交給決策器(AccessDecisionManager)
3. (可選)更改調(diào)用所根據(jù)的身份驗(yàn)證
4. 允許繼續(xù)進(jìn)行安全對(duì)象調(diào)用(假設(shè)授予了訪問(wèn)權(quán))
5. 在調(diào)用返回之后,如果配置了AfterInvocationManager。如果調(diào)用引發(fā)異常,則不會(huì)調(diào)用AfterInvocationManager。
AbstractSecurityInterceptor中的方法說(shuō)明:
beforeInvocation()方法實(shí)現(xiàn)了對(duì)訪問(wèn)受保護(hù)對(duì)象的權(quán)限校驗(yàn),內(nèi)部用到了AccessDecisionManager和AuthenticationManager;
finallyInvocation()方法用于實(shí)現(xiàn)受保護(hù)對(duì)象請(qǐng)求完畢后的一些清理工作,主要是如果在beforeInvocation()中改變了SecurityContext,則在finallyInvocation()中需要將其恢復(fù)為原來(lái)的SecurityContext,該方法的調(diào)用應(yīng)當(dāng)包含在子類請(qǐng)求受保護(hù)資源時(shí)的finally語(yǔ)句塊中。
afterInvocation()方法實(shí)現(xiàn)了對(duì)返回結(jié)果的處理,在注入了AfterInvocationManager的情況下默認(rèn)會(huì)調(diào)用其decide()方法。
了解了AbstractSecurityInterceptor,就應(yīng)該明白了,我們自定義MyFilterSecurityInterceptor就是想使用我們之前自定義的 AccessDecisionManager 和 securityMetadataSource。
SecurityConfig
@EnableWebSecurity注解以及WebSecurityConfigurerAdapter一起配合提供基于web的security。自定義類 繼承了WebSecurityConfigurerAdapter來(lái)重寫了一些方法來(lái)指定一些特定的Web安全設(shè)置。
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailsService userService; @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { //校驗(yàn)用戶 auth.userDetailsService( userService ).passwordEncoder( new PasswordEncoder() { //對(duì)密碼進(jìn)行加密 @Override public String encode(CharSequence charSequence) { System.out.println(charSequence.toString()); return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()); } //對(duì)密碼進(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"; } }
頁(yè)面
login.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <head> <meta charset="UTF-8"> <title>首頁(yè)</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>首頁(yè)</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>拒絕訪問(wèn)!</p> </div> </div> </body> </html>
最后運(yùn)行項(xiàng)目,可以分別用 user、admin 賬號(hào) 去測(cè)試認(rèn)證和授權(quán)是否正確。
總結(jié)
以上所述是小編給大家介紹的Spring Boot Security,希望對(duì)大家有所幫助,如果大家有任何疑問(wèn)請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
- Spring Boot Security 結(jié)合 JWT 實(shí)現(xiàn)無(wú)狀態(tài)的分布式API接口
- SpringBoot+Spring Security+JWT實(shí)現(xiàn)RESTful Api權(quán)限控制的方法
- Spring Boot整合Spring Security簡(jiǎn)單實(shí)現(xiàn)登入登出從零搭建教程
- Spring Boot2.0使用Spring Security的示例代碼
- SpringBoot + SpringSecurity 短信驗(yàn)證碼登錄功能實(shí)現(xiàn)
- SpringBoot結(jié)合SpringSecurity實(shí)現(xiàn)圖形驗(yàn)證碼功能
- Spring Boot Security配置教程
相關(guān)文章
Java 如何從list中刪除符合條件的數(shù)據(jù)
這篇文章主要介紹了Java 如何從list中刪除符合條件的數(shù)據(jù),具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11Spring中的注解@Autowired實(shí)現(xiàn)過(guò)程全解(@Autowired 背后的故事)
這篇文章主要介紹了Spring中的注解@Autowired實(shí)現(xiàn)過(guò)程全解,給大家聊聊@Autowired 背后的故事及實(shí)現(xiàn)原理,需要的朋友可以參考下2021-07-07JavaWeb入門教程之分頁(yè)查詢功能的簡(jiǎn)單實(shí)現(xiàn)
這篇文章主要介紹了JavaWeb入門教程之分頁(yè)查詢功能的簡(jiǎn)單實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11Java多線程中的ReentrantLock可中斷鎖詳細(xì)解讀
這篇文章主要介紹了Java多線程中的ReentrantLock可中斷鎖詳細(xì)解讀,ReentrantLock中的lockInterruptibly()方法使得線程可以在被阻塞時(shí)響應(yīng)中斷,比如一個(gè)線程t1通過(guò)lockInterruptibly()方法獲取到一個(gè)可重入鎖,并執(zhí)行一個(gè)長(zhǎng)時(shí)間的任務(wù),需要的朋友可以參考下2023-12-12SpringBoot使用Spring-Data-Jpa實(shí)現(xiàn)CRUD操作
這篇文章主要為大家詳細(xì)介紹了SpringBoot使用Spring-Data-Jpa實(shí)現(xiàn)CRUD操作,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08SpringBoot實(shí)現(xiàn)定時(shí)發(fā)送郵件的三種方法案例詳解
這篇文章主要介紹了SpringBoot三種方法實(shí)現(xiàn)定時(shí)發(fā)送郵件的案例,Spring框架的定時(shí)任務(wù)調(diào)度功能支持配置和注解兩種方式Spring?Boot在Spring框架的基礎(chǔ)上實(shí)現(xiàn)了繼承,并對(duì)其中基于注解方式的定時(shí)任務(wù)實(shí)現(xiàn)了非常好的支持,本文給大家詳細(xì)講解,需要的朋友可以參考下2023-03-03SpringBoot+微信小程序?qū)崿F(xiàn)文件上傳與下載功能詳解
這篇文章主要為大家介紹了SpringBoot整合微信小程序?qū)崿F(xiàn)文件上傳與下載功能,文中的實(shí)現(xiàn)步驟講解詳細(xì),快跟隨小編一起學(xué)習(xí)一下吧2022-03-03java中SpringBoot?自動(dòng)裝配的原理分析
這篇文章主要介紹了SpringBoot?自動(dòng)裝配的原理分析的相關(guān)資料,需要的朋友可以參考下2022-12-12