springboot整合security實現基於url的許可權控制
許可權控制基本上是任何一個web專案都要有的,為此spring為我們提供security模組來實現許可權控制,網上找了很多資料,但是提供的demo程式碼都不能完全滿足我的需求,因此自己整理了一版。
在上程式碼之前,大家需要理解兩個過程:認證和授權
使用者登陸,會被AuthenticationProcessingFilter攔截,呼叫AuthenticationManager的實現,而且AuthenticationManager會呼叫ProviderManager來獲取使用者驗證資訊(不同的Provider呼叫的服務不同,因為這些資訊可以是在資料庫上,可以是在LDAP伺服器上,可以是xml配置檔案上等),如果驗證通過後會將使用者的許可權資訊封裝一個User放到spring的全域性快取SecurityContextHolder中,以備後面訪問資源時使用。
訪問資源(即授權管理),訪問url時,會通過AbstractSecurityInterceptor攔截器攔截,其中會呼叫FilterInvocationSecurityMetadataSource的方法來獲取被攔截url所需的全部許可權,在呼叫授權管理器AccessDecisionManager,這個授權管理器會通過spring的全域性快取SecurityContextHolder獲取使用者的許可權資訊,還會獲取被攔截的url和被攔截url所需的全部許可權,然後根據所配的策略(有:一票決定,一票否定,少數服從多數等),如果許可權足夠,則返回,許可權不夠則報錯並呼叫許可權不足頁面。
整合步驟如下:
1、引入依賴和新增mybatis generator外掛
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>powerx.io</groupId> <artifactId>springboot-security</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>springboot-security</name> <description>Demo project for Spring Boot</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.5.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <!-- 分頁外掛 --> <dependency> <groupId>com.github.pagehelper</groupId> <artifactId>pagehelper-spring-boot-starter</artifactId> <version>1.2.5</version> </dependency> <!-- alibaba的druid資料庫連線池 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.1.9</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> <plugin> <groupId>org.mybatis.generator</groupId> <artifactId>mybatis-generator-maven-plugin</artifactId> <version>1.3.2</version> <configuration> <configurationFile>src/main/resources/generator/generatorConfig.xml</configurationFile> <overwrite>true</overwrite> <verbose>true</verbose> </configuration> </plugin> </plugins> </build> </project>
2、建立對應的表,標準的基於角色許可權控制的五張表,建表語句我也放到程式碼中了。
3、利用逆向工程生成對應的model、mapper和對映檔案等
4、spring security配置,關鍵位置我都加了註釋
WebSecurityConfig.java
package com.example.demo.config; import java.io.IOException; import java.io.PrintWriter; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.config.annotation.ObjectPostProcessor; 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.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import com.example.demo.service.UserService; @Configuration public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired UserService userService; @Autowired MyFilterInvocationSecurityMetadataSource myFilterInvocationSecurityMetadataSource; @Autowired MyAccessDecisionManager myAccessDecisionManager; @Autowired AuthenticationAccessDeniedHandler authenticationAccessDeniedHandler; /** * 自定義的加密演算法 * @return */ @Bean public PasswordEncoder myPasswordEncoder() { return new MyPasswordEncoder(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService).passwordEncoder(myPasswordEncoder()); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/index.html", "/static/**","/loginPage","/register"); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setSecurityMetadataSource(myFilterInvocationSecurityMetadataSource); o.setAccessDecisionManager(myAccessDecisionManager); return o; } }).and().formLogin().loginPage("/loginPage").loginProcessingUrl("/login").usernameParameter("username").passwordParameter("password").permitAll().failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); StringBuffer sb = new StringBuffer(); sb.append("{\"status\":\"error\",\"msg\":\""); if (e instanceof UsernameNotFoundException || e instanceof BadCredentialsException) { sb.append("使用者名稱或密碼輸入錯誤,登入失敗!"); } else { sb.append("登入失敗!"); } sb.append("\"}"); out.write(sb.toString()); out.flush(); out.close(); } }).successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); String s = "{\"status\":\"success\",\"msg\":\"登陸成功\"}"; out.write(s); out.flush(); out.close(); } }).and().logout().permitAll().and().csrf().disable().exceptionHandling().accessDeniedHandler(authenticationAccessDeniedHandler); } }
MyFilterInvocationSecurityMetadataSource.java
package com.example.demo.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.access.SecurityConfig; import org.springframework.security.web.FilterInvocation; import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.stereotype.Service; import com.example.demo.dao.PermissionMapper; import com.example.demo.model.Permission; import javax.servlet.http.HttpServletRequest; import java.util.*; import java.util.Map.Entry; @Service public class MyFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource { @Autowired private PermissionMapper permissionMapper; private HashMap<String, Collection<ConfigAttribute>> map = null; /** * 載入許可權表中所有許可權 */ public void loadResourceDefine() { map = new HashMap<String, Collection<ConfigAttribute>>(); List<Permission> permissions = permissionMapper.findAll(); for (Permission permission : permissions) { ConfigAttribute cfg = new SecurityConfig(permission.getPermissionname()); List<ConfigAttribute> list = new ArrayList<>(); list.add(cfg); map.put(permission.getUrl(), list); } } /** * 此方法是為了判定使用者請求的url 是否在許可權表中,如果在許可權表中,則返回給 decide 方法, 用來判定使用者 * 是否有此許可權。如果不在許可權表中則放行。 */ @Override public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException { if (map == null) { loadResourceDefine(); } // object 中包含使用者請求的request的資訊 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest(); for (Entry<String, Collection<ConfigAttribute>> entry : map.entrySet()) { String url = entry.getKey(); if (new AntPathRequestMatcher(url).matches(request)) { return map.get(url); } } return null; } @Override public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> clazz) { return true; } }
MyAccessDecisionManager.java
package com.example.demo.config; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.access.ConfigAttribute; import org.springframework.security.authentication.InsufficientAuthenticationException; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Service; import java.util.Collection; import java.util.Iterator; @Service public class MyAccessDecisionManager implements AccessDecisionManager { /** * decide 方法是判定是否擁有許可權的決策方法,authentication是CustomUserService * 中迴圈新增到 GrantedAuthority 物件中的許可權資訊集合,object 包含客戶端發起的請求的requset資訊, * 可轉換為 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest(); * configAttributes為MyFilterInvocationSecurityMetadataSource的getAttributes(Object object) * 這個方法返回的結果. * */ @Override public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { if(null== configAttributes || configAttributes.size() <=0) { return; } ConfigAttribute c; String needRole; for(Iterator<ConfigAttribute> iter = configAttributes.iterator(); iter.hasNext(); ) { c = iter.next(); needRole = c.getAttribute(); for(GrantedAuthority ga : authentication.getAuthorities()) {//authentication 為在註釋1 中迴圈新增到 GrantedAuthority 物件中的許可權資訊集合 if(needRole.trim().equals(ga.getAuthority())) { return; } } } throw new AccessDeniedException("no right"); } @Override public boolean supports(ConfigAttribute attribute) { return true; } @Override public boolean supports(Class<?> clazz) { return true; } }
AuthenticationAccessDeniedHandler.java
package com.example.demo.config; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.PrintWriter; @Component public class AuthenticationAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest httpServletRequest, HttpServletResponse resp, AccessDeniedException e) throws IOException, ServletException { resp.setStatus(HttpServletResponse.SC_FORBIDDEN); resp.setContentType("application/json;charset=UTF-8"); PrintWriter out = resp.getWriter(); out.write("{\"status\":\"error\",\"msg\":\"許可權不足,請聯絡管理員!\"}"); out.flush(); out.close(); } }
MyPasswordEncoder.java
package com.example.demo.config; import org.springframework.security.crypto.password.PasswordEncoder; public class MyPasswordEncoder implements PasswordEncoder { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return s.equals(charSequence.toString()); } }
UserServiceImpl.java
package com.example.demo.service.impl; 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.UsernameNotFoundException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.demo.dao.PermissionMapper; import com.example.demo.dao.RoleMapper; import com.example.demo.dao.UserMapper; import com.example.demo.model.Permission; import com.example.demo.model.User; import com.example.demo.service.UserService; @Service public class UserServiceImpl implements UserService { @Autowired private PermissionMapper permissionMapper; @Autowired private RoleMapper roleMapper; @Autowired private UserMapper userMapper; @Autowired private PasswordEncoder passwordEncoder; @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userMapper.selectByUsername(username); if (user != null) { List<Permission> permissions = permissionMapper.findByUserId(user.getId()); List<GrantedAuthority> grantedAuthorities = new ArrayList <>(); for (Permission permission : permissions) { if (permission != null && permission.getPermissionname()!=null) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getPermissionname()); grantedAuthorities.add(grantedAuthority); } } return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), grantedAuthorities); } else { throw new UsernameNotFoundException("username: " + username + " do not exist!"); } } @Transactional @Override public void userRegister(String username, String password) { User user = new User(); user.setUsername(passwordEncoder.encode(username)); user.setPassword(password); userMapper.insert(user); User rtnUser =userMapper.selectByUsername(username); //註冊成功預設給使用者的角色是user roleMapper.insertUserRole(rtnUser.getId(), 2); } }
至此,整合基本完畢,其它控制層的程式碼和mapper層的程式碼不再貼出,需要注意的是註冊使用者的時候我們要用自定義的加密工具對密碼進行加密(當然在demo中我什麼也沒做),其它的一些功能比如給使用者加角色、給角色加許可權等的增刪改查,大家可以根據需要自行新增,另外在permissionMapper.findByUserId(user.getId())這裡我寫了一個五張表的關聯查詢,可以根據userid可以查出使用者所有對應的許可權。