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可以查出用户所有对应的权限。
为了方便大家和自己以后参考,代码已上传至码云:https://gitee.com/hehang_com/springboot-security。