springboot-29-security(二)用户角色权限控制
本博客基于上一个 http://www.cnblogs.com/wenbronk/p/7379865.html
增加了角色的权限表, 可以进行权限校验
一, 数据准备
1, 数据表建立
/* Navicat MySQL Data Transfer Source Server : 本地 Source Host : localhost:3306 Source Database : test Target Server Type : MYSQL Date: 2017-8-14 22:17:33 */ SET FOREIGN_KEY_CHECKS=0; -- ---------------------------- -- Table structure for `sys_user` -- ---------------------------- DROP TABLE IF EXISTS `sys_user`; CREATE TABLE `sys_user` ( `id` INT (32) NOT NULL AUTO_INCREMENT COMMENT '主键id', `username` varchar(32) DEFAULT NULL COMMENT '用户名', `password` varchar(32) DEFAULT NULL COMMENT '密码', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for `sys_role` -- ---------------------------- DROP TABLE IF EXISTS `sys_role`; CREATE TABLE `sys_role` ( `id` INT (32) NOT NULL AUTO_INCREMENT COMMENT '主键id', `name` varchar(32) DEFAULT NULL COMMENT '用户名', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for `sys_permission` -- ---------------------------- DROP TABLE IF EXISTS `sys_permission`; CREATE TABLE `sys_permission` ( `id` INT (32) NOT NULL AUTO_INCREMENT COMMENT '主键id', `name` varchar(32) DEFAULT NULL COMMENT '用户名', `desc` VARCHAR (32) DEFAULT NULL COMMENT '描述', `url` VARCHAR (32) DEFAULT NULL COMMENT 'url', `pid` INT (32), PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8; -- ---------------------------- -- Table structure for `sys_role_user` -- ---------------------------- DROP TABLE IF EXISTS `sys_role_user`; CREATE TABLE `sys_role_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id', `sys_user_id` INT(32) NOT NULL COMMENT 'user_id', `sys_role_id` INT(32) NOT NULL COMMENT 'role_id', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8; ALTER TABLE sys_role_user ADD CONSTRAINT sys_FK1 FOREIGN KEY(sys_user_id) REFERENCES sys_user(id); ALTER TABLE sys_role_user ADD CONSTRAINT role_FK2 FOREIGN KEY(sys_role_id) REFERENCES sys_role(id); -- ---------------------------- -- Table structure for `sys_permission_role` -- ---------------------------- DROP TABLE IF EXISTS `sys_permission_role`; CREATE TABLE `sys_permission_role` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键id', `sys_role_id` INT(32) NOT NULL COMMENT 'role_id', `sys_permission_id` INT(32) NOT NULL COMMENT 'permission_id', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=28 DEFAULT CHARSET=utf8; ALTER TABLE sys_permission_role ADD CONSTRAINT sys_FK3 FOREIGN KEY(sys_role_id) REFERENCES sys_role(id); ALTER TABLE sys_permission_role ADD CONSTRAINT role_FK4 FOREIGN KEY(sys_permission_id) REFERENCES sys_permission(id);
2, 导入数据
insert into SYS_USER (id,username, password) values (1,'vini', '123'); insert into SYS_USER (id,username, password) values (2,'bronk', '123'); insert into SYS_ROLE(id,name) values(1,'ROLE_ADMIN'); insert into SYS_ROLE(id,name) values(2,'ROLE_USER'); insert into SYS_ROLE_USER(SYS_USER_ID,sys_role_id) values(1,1); insert into SYS_ROLE_USER(SYS_USER_ID,sys_role_id) values(2,2); BEGIN; INSERT INTO `sys_permission` VALUES ('1', 'ROLE_HOME', 'home', '/', null), ('2', 'ROLE_ADMIN', 'ABel', '/admin', null); COMMIT; BEGIN; INSERT INTO `sys_permission_role` VALUES ('1', '1', '1'), ('2', '1', '2'), ('3', '2', '1'); COMMIT;
3, mybatis实体, 其余2个和上一篇博客一样
SysPermission.groovy
package com.wenbronk.security.entity /** * Created by wenbronk on 2017/8/17. */ class SysPermission { int id String name String desc String url int pid }
4, application.yml配置服务启动导入
和上个一样
二, security部分
东西比较多, 按流程来,
1, WebSecurityConfig.groovy 添加
http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class)
最终代码为:
package com.wenbronk.security.security.config import com.wenbronk.security.security.interceptor.MyFilterSecurityInterceptor import com.wenbronk.security.security.service.CustomUserService 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.configuration.EnableWebSecurity import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter import org.springframework.security.web.access.intercept.FilterSecurityInterceptor import javax.inject.Inject /** * Created by wenbronk on 2017/8/15. */ @Configuration @EnableWebSecurity class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Inject CustomUserService customUserService; @Autowired MyFilterSecurityInterceptor myFilterSecurityInterceptor @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(customUserService); } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated() // 任何请求都拦截 .and() .formLogin() .loginPage("/login") .failureUrl("/login?error") .permitAll() // 登陆后可访问任意页面 .and() .logout().permitAll(); // 注销后任意访问 http.addFilterBefore(myFilterSecurityInterceptor, FilterSecurityInterceptor.class) } }
2, 请求会被拦截器拦截, MyFilterSecurityInterceptor,
package com.wenbronk.security.security.interceptor import com.wenbronk.security.security.config.MyAccessDecisonManager import org.springframework.beans.factory.annotation.Autowired import org.springframework.security.access.SecurityMetadataSource import org.springframework.security.access.intercept.AbstractSecurityInterceptor import org.springframework.security.access.intercept.InterceptorStatusToken import org.springframework.security.web.FilterInvocation import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource import org.springframework.stereotype.Component import javax.inject.Inject import javax.servlet.* /** * Created by wenbronk on 2017/8/17. */ @Component class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter { @Inject FilterInvocationSecurityMetadataSource securityMetadataSource @Autowired public void setMyAccessDecisionManager(MyAccessDecisonManager myAccessDecisonManager) { super.setAccessDecisionManager(myAccessDecisonManager) } @Override void init(FilterConfig filterConfig) throws ServletException { } /** * fi中有一个被拦截的url * 里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限 //再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够 */ @Override void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { FilterInvocation fi = new FilterInvocation(request, response, chain) invoke(fi) } void invoke(FilterInvocation filterInvocation) { InterceptorStatusToken token = super.beforeInvocation(filterInvocation) try { filterInvocation.getChain().doFilter(filterInvocation.getRequest(), filterInvocation.getResponse()) } finally { super.afterInvocation(token, null) } } @Override void destroy() { } @Override Class<?> getSecureObjectClass() { return FilterInvocation.class } @Override SecurityMetadataSource obtainSecurityMetadataSource() { return this.securityMetadataSource } }
3, 拦截器会调用 MyInvocationSecurityMetaDataSource的 getAttribute方法 获取 filter的权限, 并且滴哦啊用 MyAccessDecisionManager的 decide 方法来校验是否拥有权限
MyInvocationSecurityMetadataSource.groovy
package com.wenbronk.security.security.service import com.wenbronk.security.entity.SysPermission import com.wenbronk.security.mapper.SysPermissionMapper 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 javax.inject.Inject import javax.servlet.http.HttpServletRequest /** * Created by wenbronk on 2017/8/17. */ @Service class MyInvocationSecurityMetadataSourceService implements FilterInvocationSecurityMetadataSource{ @Inject SysPermissionMapper sysPermissionMapper /** * 此方法是为了判定用户请求的url 是否在权限表中, * 如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。 */ @Override Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { Map<String, Collection<ConfigAttribute>> map = loadResourceDefine() HttpServletRequest request = ((FilterInvocation) o).getHttpRequest() map.each {entry -> AntPathRequestMatcher matcher = new AntPathRequestMatcher(entry.getKey()) if (matcher.matches(request)) { // 匹配, 返回给decide方法 return entry.getValue() } } return null } /** * 加载权限表中所有的权限 */ public Map<String, Collection<ConfigAttribute>> loadResourceDefine() { Map<String, Collection<ConfigAttribute>> map = new HashMap<>() List<SysPermission> permissions = sysPermissionMapper.findAll() permissions.each {permission -> List<ConfigAttribute> array = new ArrayList<>() // 此处只添加了用户的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。 // 此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。 ConfigAttribute cfg = new SecurityConfig(permission.getName()) array.add(cfg) // 用权限的url作为key, 权限的名称集合为value map.put(permission.getUrl(), array); } return null } @Override Collection<ConfigAttribute> getAllConfigAttributes() { return null } @Override boolean supports(Class<?> aClass) { return true } }
4, MyInvocationSecurityMetadataSource 查询数据库中url和所需权限的关系, 返回给 MyAccessDecisionManger
MyAccessDecisionManger.groovy
package com.wenbronk.security.security.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.stereotype.Service /** * Created by wenbronk on 2017/8/17. */ @Service class MyAccessDecisonManager implements AccessDecisionManager { /** * 判断是否拥有权限的决策方法 * authentication 是CustomUserService中循环添加到 GrantedAuthority 对象中的权限信息集合. * object 包含客户端发起的请求的requset信息,可转换为 HttpServletRequest request = ((FilterInvocation) object).getHttpRequest(); * configAttributes 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果, */ @Override void decide(Authentication authentication, Object o, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException { if (null == configAttributes || configAttributes.size() <=0 ) return configAttributes.each {configAttribute -> String needRole = configAttribute.getAttribute() authentication.getAuthorities().each {authority -> if (needRole.trim().equals(authority.getAuthority())) return } } throw new java.nio.file.AccessDeniedException('no right') } @Override boolean supports(ConfigAttribute configAttribute) { return true } @Override boolean supports(Class<?> aClass) { return true } }
decide进行是否有权限的决策, 正常则返回, 没有就抛出异常
5, 最终在 customuserService进行权限和用户的装配, 准备返回给前端
package com.wenbronk.security.security.service import com.wenbronk.security.entity.SysPermission import com.wenbronk.security.entity.SysUser import com.wenbronk.security.mapper.SysPermissionMapper import com.wenbronk.security.mapper.SysUserMapper import org.springframework.security.core.GrantedAuthority import org.springframework.security.core.authority.SimpleGrantedAuthority import org.springframework.security.core.userdetails.User 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 javax.inject.Inject /** * Created by wenbronk on 2017/8/15. */ @Service class CustomUserService implements UserDetailsService { @Inject SysUserMapper sysUserMapper @Inject SysPermissionMapper sysPermissionMapper @Override UserDetails loadUserByUsername(String s) throws UsernameNotFoundException { def sysUser = sysUserMapper.findByUserName(s) as SysUser if (sysUser != null) { List<SysPermission> permissions = sysPermissionMapper.findByAdminUserId(sysUser.getId()) List<GrantedAuthority> grantedAuthorities = new ArrayList<>() permissions.each {permission -> if (permission != null && permission.getName() != null) { GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(permission.getName()) // 此处将权限信息添加到 GrantedAuthority 对象中,在后面进行全权限验证时会使用GrantedAuthority 对象。 grantedAuthorities.add(grantedAuthority) } } return new User(sysUser.getUsername(), sysUser.getPassword(), grantedAuthorities) }else { throw new UsernameNotFoundException('admin: ' + s + ' do not exits') } } }
三: 页面部分
1, 页面转向config, 和上一篇的一样
@Configuration class WebMvcConfig extends WebMvcConfigurerAdapter{ @Override void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/login").setViewName("login") // registry.addViewController("/home").setViewName("home") } }
2, 更改home.html
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"> <head> <meta content="text/html;charset=UTF-8"/> <title sec:authentication="name"></title> <link rel="stylesheet" th:href="@{css/bootstrap.min.css}" /> <style type="text/css"> body { padding-top: 50px; } .starter-template { padding: 40px 15px; text-align: center; } </style> </head> <body> <nav class="navbar navbar-inverse navbar-fixed-top"> <div class="container"> <div class="navbar-header"> <a class="navbar-brand" href="#">Spring Security演示</a> </div> <div id="navbar" class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li><a th:href="@{/}"> 首页 </a></li> <li><a th:href="@{/admin}"> admin </a></li> </ul> </div><!--/.nav-collapse --> </div> </nav> <div class="container"> <div class="starter-template"> <h1 th:text="${msg.title}"></h1> <p class="bg-primary" th:text="${msg.content}"></p> <div sec:authorize="hasRole('ROLE_HOME')"> <!-- 用户类型为ROLE_ADMIN 显示 --> <p class="bg-info" th:text="${msg.etraInfo}"></p> </div> <div sec:authorize="hasRole('ROLE_ADMIN')"> <!-- 用户类型为 ROLE_USER 显示 --> <p class="bg-info">恭喜, 有ROLE_ADMIN的权限</p> </div> <form th:action="@{/logout}" method="post"> <input type="submit" class="btn btn-primary" value="注销"/> </form> </div> </div> </body> </html>
四, 密码进行加密
注意, 如果使用, 存储进数据库的密码也必须是同一个算法计算的密文
md5, 工具类
package com.wenbronk.security.tools import java.security.MessageDigest /** * Created by wenbronk on 2017/8/17. */ class MD5Utils { private static final String SALT = "tamboo"; public static String encode(String password) { password = password + SALT; MessageDigest md5 = null; try { md5 = MessageDigest.getInstance("MD5"); } catch (Exception e) { throw new RuntimeException(e); } char[] charArray = password.toCharArray(); byte[] byteArray = new byte[charArray.length]; for (int i = 0; i < charArray.length; i++) byteArray[i] = (byte) charArray[i]; byte[] md5Bytes = md5.digest(byteArray); StringBuffer hexValue = new StringBuffer(); for (int i = 0; i < md5Bytes.length; i++) { int val = ((int) md5Bytes[i]) & 0xff; if (val < 16) { hexValue.append("0"); } hexValue.append(Integer.toHexString(val)); } return hexValue.toString(); } public static void main(String[] args) { System.out.println(MD5Utils.encode("abel")); } }
2, 修改 webSecurityConfig的加密方法
@Override protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(customUserService()).passwordEncoder(new PasswordEncoder(){ @Override public String encode(CharSequence rawPassword) { return MD5Util.encode((String)rawPassword); } @Override public boolean matches(CharSequence rawPassword, String encodedPassword) { return encodedPassword.equals(MD5Util.encode((String)rawPassword)); }}); //user Details Service验证 }
BCrypt的强hash算法
BCrypt强哈希方法 每次加密的结果都不一样。但是存贮其中一次加密结果 也能够验证成功
1, 修改WebSecurityConfig
@Autowired protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(customUserService).passwordEncoder(new BCryptPasswordEncoder()); }
2, 进行加密
public SysUser create(User u user){ //进行加密 BCryptPasswordEncoder encoder =new BCryptPasswordEncoder(); sysUser.setPassword(encoder.encode(user.getRawPassword().trim())); userDao.create(user); return sysUser;
原博客地址: