赞
踩
全名为:Role-Based Access Control 译为基于角色的访问控制。
RBAC权限框架基于角色进行鉴权,在该框架中具有三大模块:角色(Role)、用户(User)、权限(Permissions),
RBAC使用最小特权原则,当前请求访问的用户具备那些角色,该角色具备那些权限,所具备的权限中是否包含本次访问所需的权限?若具有,正常访问返回,若不具有,给予用户提示,所以,RBAC可以把权限粒度做到方法级。
SpringSecurity是基于RBAC模型轻量级权限控框架,与之对等的还有Apache Shiro,由于Spring的生态不断完善、功能日益丰富,使得SpringSecurity越来越越受欢迎。
一般的,SpringSecurity的权限控制设计思路为:User - User_Role -Role -Role_Menu -Menu,即:用户属于什么角色,该角色具有什么权限,具有该权限可以访问那些页面,如若把权限控制在方法级别,可以使用SpringSecurity注解在后端方法上,从而做到按钮级别的权限控制,以上,便完成了权限访问控制。
数据库设计便为:
(用户可能有多个角色,一个角色可能有多个用户,所以用户和角色是多对多的关系)
Menu可以理解为权限,在Web中,菜单中的显示与否可以视为用户是否具备该权限
如此便完成了权限控制的设计方案。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
加入这个依赖后表示所有的接口都是被保护的状态,访问的时候被Security拦截。
在浏览器输入该请求路径,会自重定向到Spring Security的登录页。默认的用户名是user,密码请去IDEA的Consolse去找项目每次启动时随机生成的字符串:
Using generated security password: 5a38aea2-81d0-485d-bf5c-12c73b0aad27
(复制passwor后的内容即可访问)
同时也支持在数据库配置用户名和密码(正式项目一般处理方式)或在配置文件配置用户名密码,本文使用的是yml配置,properties同理,如果不知道如何配置,请自行百度。配置后在Console中便不自动生成password
配置接口放行规则
SecurityConfig.class
@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UserService userService; @Autowired private RedisService redisUtil; @Autowired private SecurityFilter securityFilter; @Autowired private OwnAccessDecisionManager ownAccessDecisionManager; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userService); } @Bean PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean /** * 定义角色继承 */ RoleHierarchy roleHierarchy() { RoleHierarchyImpl roleHierarchy = new RoleHierarchyImpl(); roleHierarchy.setHierarchy("ROLE_dba> ROLE_admin > ROLE_user"); return roleHierarchy; } /** * 配置HTTP请求规则 * 什么请求路径需要什么权限才能访问, * 登录接口,都可访问 * * @param http * @throws Exception */ @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .antMatchers("/admin/**").hasRole("admin") .antMatchers("user/**").hasAnyRole("admin", "user") .anyRequest().authenticated() .and() .formLogin() .loginProcessingUrl("/doLogin") .usernameParameter("userName") .passwordParameter("passWord") .successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { //登陆成功处理句柄,前后分离项目,给前端返回Json即可 resp.setContentType("application/json;charset=utf-8"); Map<String, Object> map = new HashMap<>(); map.put("status", HttpServletResponse.SC_OK); User principal = (User) authentication.getPrincipal(); String token = JwtUtil.sign(principal.getUsername(), principal.getPassword()); map.put("msg", authentication.getPrincipal()); map.put("token", token); map.put("userName", principal.getUsername()); redisUtil.setCacheObject(BusinessConstant.REDIS_RELATED.PREFIX + LocalDateUtils.getStartTimeOfDayStr() + principal.getId(), map); ResponseUtil.responseJson(resp, HttpStatus.OK.value(), map); } }) .failureHandler(new AuthenticationFailureHandler() { @Override public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse resp, AuthenticationException e) throws IOException, ServletException { //登录失败处理 AuthenticationException:锁定异常问题 resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); Map<String, Object> map = new HashMap<>(); map.put("status", HttpServletResponse.SC_UNAUTHORIZED); if (e instanceof LockedException) { map.put("msg", ResponseEnum.USER_ACCOUNT_LOCKED.getMessage()); } else if (e instanceof BadCredentialsException) { map.put("msg", ResponseEnum.USER_NOT_EXIST_OR_ERROR.getMessage()); } else { map.put("msg", ResponseEnum.LOGIN_FILURE.getMessage()); } out.write(new ObjectMapper().writeValueAsString(map)); out.flush(); out.close(); } }) .permitAll() .and() .logout() .logoutUrl("/logout") .logoutSuccessHandler(new LogoutSuccessHandler() { @Override public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse resp, Authentication authentication) throws IOException, ServletException { resp.setContentType("application/json;charset=utf-8"); PrintWriter out = resp.getWriter(); out.write(new ObjectMapper().writeValueAsString("注销成功!")); out.flush(); out.close(); } }) .withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() { @Override public <O extends FilterSecurityInterceptor> O postProcess(O o) { o.setAccessDecisionManager(ownAccessDecisionManager); o.setSecurityMetadataSource(securityFilter); return o; } }) .and() .csrf().disable() .exceptionHandling() //没有权限时返回Json,而不是重定向到登录页,方便前后端分离项目使用 .authenticationEntryPoint(new AuthenticationEntryPoint() { @Override public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authException) throws IOException, ServletException { httpServletResponse.setContentType("application/json;charset=utf-8"); PrintWriter out = httpServletResponse.getWriter(); if (authException instanceof InsufficientAuthenticationException) { throw new RuntimeException(ResponseEnum.SYSTEM_INNER_ERROR.getMessage()); } // ResponseEnum.SYSTEM_INNER_ERROR.assertException(authException); out.write(new ObjectMapper().writeValueAsString(authException)); out.flush(); out.close(); } }); }
SecurityFilter.class
根据请求地址分析出该地址需要那些角色,并查看请求的用户是否具备该角色
@Component public class SecurityFilter implements FilterInvocationSecurityMetadataSource{ @Autowired private MenuService menuService; @Autowired private RedisService redisService; /** * Ant规则匹配符 */ AntPathMatcher pathMatcher = new AntPathMatcher(); @Override public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException { String requestUrl = ((FilterInvocation) o).getRequestUrl(); List<Menu> allMenus; allMenus = redisService.getCacheObject(BusinessConstant.REDIS_RELATED.MENU_ALL); if (CollectionUtils.isEmpty(allMenus)) { allMenus = menuService.getAllMenus(); } for (Menu menu : allMenus) { if (pathMatcher.match(menu.getPattern(), requestUrl)) { List<Role> roles = menu.getRoles(); String[] rolesStr = new String[roles.size()]; for (int i = 0; i < roles.size(); i++) { rolesStr[i] = roles.get(i).getName(); } return SecurityConfig.createList(rolesStr); } } //需要的角色都不满足条件,非法请求 return SecurityConfig.createList("ROLE_login"); } @Override /** *根据需要的角色查看当前用户是否具有该角色 */ public Collection<ConfigAttribute> getAllConfigAttributes() { return null; } @Override public boolean supports(Class<?> aClass) { return true; } }
OwnAccessDecisionManager.class
为当前的访问规则进行决策,是否给予访问的权限。
@Component public class OwnAccessDecisionManager implements AccessDecisionManager { @Override public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException, InsufficientAuthenticationException { for (ConfigAttribute attribute : collection) { if ("ROLE_login".equals(attribute.getAttribute())) { if (authentication instanceof AnonymousAuthenticationToken) { throw new AccessDeniedException(ResponseEnum.PERMISSION_NOT_SAFE.getMessage()); } else { return; } } //查询访问所需角色,当前登录用户是否具备所需角色的其中的一个 Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities(); for (GrantedAuthority authority : authorities) { if (authority.getAuthority().equals(attribute.getAttribute())) { return; } } throw new AccessDeniedException(ResponseEnum.USER_ROLE_PERMISSION_ERROR.getMessage()); } } @Override public boolean supports(ConfigAttribute configAttribute) { return true; } @Override public boolean supports(Class<?> aClass) { return true; } }
使用postman测试,所以关闭CSRF攻击,正式环境请开启 记得要删掉super.configure(http); 不然会报错IllegalStateException: Can't configure anyRequest after itself ObjectMapper类是Jackson库的主要类。它提供一些功能将转换成Java对象匹配JSON结构,反之亦然。它使用JsonParser和JsonGenerator的实例实现JSON实际的读/写。
- 1
- 2
- 3
使用post请求构造表单登录,SpringSecurity已做密码脱敏,权限中默认使用"ROLE_"为前缀。
登出配置如上代码,构造get请求即可。
由于篇幅原因,不宜过长,所以我是分开书写的,权限功能需要整合数据库相关,在我的另一篇文章中:
SpringBoot整合Redis、MyBatis-Plus
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。