利用spring security实现动态权限控制
前言:利用security实现权限控制
一,准备数据库MySql
1,创建五张表,user表、role表、uer_role表、menu表、menu_role表
user表:用来存放用户信息,字段id、name、username、password、enabled等
说明:必要的字段username和password,这两个字段表示登陆时需要验证的内容。
在登陆页面输入的用户名和密码即为这两个字段,name字段表示该用户的角色,
是管理员还是其他角色等,可自定义。enabled字段表示该用户是否启用。
role表:字段id、name和nameZh
说明:name表示角色英文名称,nameZh表示中文名称,记住,name字段必须得以
ROLE_开头;如下图:
user_role表:用户角色关联表,字段id、uid和rid,uid和rid分别设置两个外键对应user表和role表的id。
说明:该表的意义在于在用户登陆时可以通过该表查找到此用户的角色。
menu表:菜单表,其实是将后端的接口地址保存在数据库,如下图:
其中url为后端接口地址,接口地址采取ant风格,和spring security的要求统一。
说明:该表非必需,但是为了好动态的控制权限,设置该表为角色动态添加功能。
menu_role表:菜单角色关联表,字段id、mid和rid
说明:该表用于通过角色查找对应的菜单,表示该角色能够访问哪些菜单项。实现权限控制,mid和rid分别设置外键和menu表id、role表id关联。
二,编写代码
1,创建实体类User、Role、Menu;根据字段创建,生成get和set方法。
其中Role和Menu正常创建即可。重点在User类
User类:创建完User类后,需要实现spring 中UserDetails类,并且实现里面的七个方法
如下源码图
实现里面的方法后,这里需要注意的是,这里面已经有了getPassword、getUsername和isEnabled,你在自动生成get和set时会和实现UserDetails时重复,需要把自动生成的删掉,不然就有两个对应的get方法。
接下来就是UserService类了,该类需要实现UserDetailsService类,该类的源码:
其中只有一个方法需要实现,loadUserByUsername(String usernam),见名知意,通过用户名查找用户信息。
在UserService中实现该方法后,需要注入UserMapper类
我这里的Hr其实就是User,loadUserByUsername该方法返回的是UserDetails对象,因为,你的User类实现了UserDetails类,所以这里需要返回User,接下来就是配置securityConfig配置类
标号1,注入你的UserService,我这里的Hr其实就是User,
标号2,返回spring security的密码加密实例,注意@bean注解,告诉spring这里使用的是
BCryptPasswordEncoder加密方式,这个加密方式生成的密文每次都不一样,比喻123加密后
生成的密文每次不一致,可能你会问那登陆怎么匹配呢?其实,它在生成密文时会加上一个
随机数,并且把随机数保存,登陆匹配时,再次提取即可。源码如下:
// Seed Generator
private static volatile SecureRandom seedGenerator = null;
/**
* Constructs a secure random number generator (RNG) implementing the
* default random number algorithm.
*
* <p> This constructor traverses the list of registered security Providers,
* starting with the most preferred Provider.
* A new SecureRandom object encapsulating the
* SecureRandomSpi implementation from the first
* Provider that supports a SecureRandom (RNG) algorithm is returned.
* If none of the Providers support a RNG algorithm,
* then an implementation-specific default is returned.
*
* <p> Note that the list of registered providers may be retrieved via
* the {@link Security#getProviders() Security.getProviders()} method.
*
* <p> See the SecureRandom section in the <a href=
* "{@docRoot}/../technotes/guides/security/StandardNames.html#SecureRandom">
* Java Cryptography Architecture Standard Algorithm Name Documentation</a>
* for information about standard RNG algorithm names.
*
* <p> The returned SecureRandom object has not been seeded. To seed the
* returned object, call the {@code setSeed} method.
* If {@code setSeed} is not called, the first call to
* {@code nextBytes} will force the SecureRandom object to seed itself.
* This self-seeding will not occur if {@code setSeed} was
* previously called.
*/
标3:该方法进行用户信息验证,进行密码匹配,将我们注入的UserService注入即可。
标4:此方法为重点,进行权限控制。主要是对请求进行控制验证和反馈。
http.authorizeRequests() //对请求授权,表示接下来需要配置授权规则
.anyRequest().authenticated()//表示,任何请求都需要通过我配置的授权规则才能访问,才能被系统认证
ok,路径规则配置完后,接下来需要配置登陆页面
.formLogin()//配置表单登陆
.usernameParameter("username")//登陆时的参数,springsecurity必须是post请求登陆
.passwordParameter("password")//这两个参数定义表示你传递的key
//springsecurity 默认就是username来接收用户输入的用户名,password接收用户输入密码
//你也可以设置成其他的
.loginProcessingUrl("/doLogin")
//发送用户输入的信息的路径,用户发送的信息,springsecurity会自动处理,
//不需要我们去写controller进行密码匹配的操作了
.loginPage("/login")
//配置登陆页面,如果前端需要自定义登陆页面这个必须配置
//你只需要在前端访问该路径时,controller里面返回login.html或者login.ftl即可
如果将usernameParameter和passwordParameter设置为自定义的,你在前端通过表单或者ajax发送的key就得跟自定义的保持一致。
那么登陆成功后该如何处理呢?这里有两种方式:
1,一般表单登陆的话,就可以登陆成功后直接跳转到首页,只需要配置如下方法即可
.successForwardUrl("/index")
2,如果是ajax登陆,那么返回的就是json数据了
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest req, HttpServletResponse res, Authentication authentication) throws IOException, ServletException {
res.setContentType("application/json;charset=utf-8");
PrintWriter o = res.getWriter();
Hr hr = (Hr) authentication.getPrincipal();//拿到用户对象包含了该用户的所有信息
hr.setPassword(null);//将密码设置为null,密码不返回给前端
RespBean ok = RespBean.ok("登陆成功", hr);//处理返回信息
String s = new ObjectMapper().writeValueAsString(ok);//将RespBean 实例转换成字符串,因为前端接收的是json,其本质为字符串
o.write(s);
o.flush();
o.close();
}
})
RespBean 类,该类处理所有返回信息
/***
* 单例模式
*/
public class RespBean {
private Integer status;
private String msg;
private Object obj;
public static RespBean ok(String msg){
return new RespBean(200,msg,null);
}
public static RespBean ok(String msg,Object obj){
return new RespBean(200,msg,obj);
}
public static RespBean error(String msg){
return new RespBean(500,msg,null);
}
public static RespBean error(String msg,Object obj){
return new RespBean(200,msg,obj);
}
private RespBean() {
}
private RespBean(Integer status, String msg, Object obj) {
this.status = status;
this.msg = msg;
this.obj = obj;
}
//省略getter和setter ...
}
有成功自然有失败,道理和成功一样有两种方式,
1,跳转错误页面
.failureForwardUrl("/error")
2,通过返回json,适用于ajax
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest req, HttpServletResponse res, AuthenticationException e) throws IOException, ServletException {
res.setContentType("application/json;charset=utf-8");
PrintWriter o = res.getWriter();
RespBean error = RespBean.error("登陆失败");
if (e instanceof LockedException) { //判断e属于哪个异常的实例来确定是何错误。
error.setMsg("账户被锁定,请联系管理员!");
} else if (e instanceof CredentialsExpiredException) {
error.setMsg("密码过期,请联系管理员");
} else if (e instanceof AccountExpiredException) {
error.setMsg("账户过期,请联系管理员");
} else if (e instanceof UsernameNotFoundException) {
error.setMsg("用户名未找到,请重新输入!");
} else if (e instanceof DisabledException) {
error.setMsg("账户被禁用,请联系管理员");
} else if (e instanceof BadCredentialsException) {
error.setMsg("用户名或密码输入错误,请重新输入!");
}
o.write(new ObjectMapper().writeValueAsString(error));
o.flush();
o.close();
}
})
.permitAll()//表示登陆的请求和页面路径任何人都允许访问
配置用户登出:
.logout()
.logoutUrl("/logout")//登出路径,默认就是/logout 可以不配置
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest req, HttpServletResponse res, Authentication authentication) throws IOException, ServletException {
res.setContentType("application/json;charset=utf-8");
PrintWriter o = res.getWriter();
o.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销成功!")));
o.flush();
o.close();
}
})
.csrf().disable()//关闭csrf(跨站点请求伪造)保护,
如果未登录,直接通过地址栏输入地址访问spring security默认重定向到登陆页/login
我这里不需要重定向所以,进行一下配置
//处理未登录 直接通过地址栏访问,返回提示信息,不重定向到登录页
.exceptionHandling().authenticationEntryPoint(new AuthenticationEntryPoint() {
@Override
public void commence(HttpServletRequest req, HttpServletResponse res, AuthenticationException e) throws IOException, ServletException {
res.setContentType("application/json;charset=utf-8");
res.setStatus(401);
PrintWriter o = res.getWriter();
RespBean error = RespBean.error("登陆失败");
if (e instanceof InsufficientAuthenticationException) {
error.setMsg("未登录,访问失败!");
}
o.write(new ObjectMapper().writeValueAsString(error));
o.flush();
o.close();
}
标5:该方法对静态资源访问进行配置,一般在/resource/static/下面的静态资源允许用户访问,忽略请求认证。
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/login")
.antMatchers("/static/**");
}
ok,那么如何实现,管理员权限大于普通用户呢,比喻说,如果是管理员,应该能访问用户的资源,但是,如果我并没有配置如下代码
.antMatchers("/user/**").hasAnyRole("ROLE_admin","ROLE_user")
而只是配置了,
.antMatchers("/user/**").hasRole("ROLE_user")
那么管理员访问不了,user下面的资源了,那这怎么办呢?
springsecurity 当然也考虑到了,可以配置角色继承
@Bean
RoleHierarchy roleHierarchy(){
RoleHierarchy roleHierarchy = new RoleHierarchyImpl();
String hierarchy = "ROLE_admin > ROLE_user";
//如果有多个的话
// String hierarchy = "ROLE_admin > ROLE_user \n ROLE_user > ROLE_test1";
//这种方式适合spring 5及以上
((RoleHierarchyImpl) roleHierarchy).setHierarchy(hierarchy);
return roleHierarchy;
}
到此基本配置结束了,那么,你有没有想过这样一个问题:比喻说要做一个公司的人事管理系统,里面有部门经理、招聘主管、培训主管和系统管理员,这些角色访问的资源肯定不一样,而且又包含关系,也有交集对吧。前面我们说过利用如下配置进行权限设置,但是这样写死的每个用户登陆,他访问的路径都不一样,怎么处理?这就需要动态权限管理了,当然如果你的项目不是那种权限很细的,其实这种够用了。
.antMatchers("/admin/**").hasRole("ROLE_admin")//拥有ROLE_admin 角色才能访问/admin/**匹配的路径
.antMatchers("/user/**").hasAnyRole("ROLE_admin","ROLE_user")
如何配置动态权限呢?首先,创建MyFilter类,加上注解@component,并且implements FilterInvocationSecurityMetadataSource类 FilterInvocationSecurityMetadataSource类继承SecurityMetadataSource类,需要实现三个方法:如下源码
@Component
public class MyFilter implements FilterInvocationSecurityMetadataSource {
@Autowired
MenuService menuService;//注入菜单表,菜单表里面为后端方法路径
AntPathMatcher antPathMatcher = new AntPathMatcher();//路径匹配的工具类,spring security里面有,无需自己写
@Override
public Collection<ConfigAttribute> getAttributes(Object o) throws IllegalArgumentException {
String requestUrl = ((FilterInvocation) o).getRequestUrl(); //获取用户的访问路径
List<Menu> menus = menuService.getAllMenusWithRole();//查询所有菜单以及对应的角色
//sql语句为:
//SELECT m.*, r.id AS rid,r.`name` AS rname,r.nameZh AS rnameZh
//FROM menu m,menu_role mr,role r WHERE m.id = mr.mid
//AND mr.rid = r.id ORDER BY m.id;
for (Menu menu : menus) {
if (antPathMatcher.match(menu.getUrl(),requestUrl)){ //比较菜单路径和请求路径是否一致
List<Role> roles = menu.getRoles();//如果匹配到了,拿到该路径对应的角色
String[] str = new String[roles.size()];
for (int i = 0;i<roles.size();i++) {
str[i] = roles.get(i).getName();//将角色名赋值给str
}
return SecurityConfig.createList(str);//创建角色属性列表
}
}
//1,如果所有请求地址都没有匹配上的话,可以return null
//2,返回SecurityConfig.createList("ROLE_LOGIN");
//ROLE_LOGIN 只是一个标记,后边判断返回值 如果是ROLE_LOGIN的话 则让用户登陆后访问
return SecurityConfig.createList("ROLE_LOGIN");
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null; //返回null即可
}
@Override
public boolean supports(Class<?> aClass) {
return true;/返回true即可
}
}
该类并没有做实质性的权限控制,只是将该请求地址所对应的角色添加到spring的安全元数据属性列表上,方便在spring security 上下文调用。接下来进行权限控制
创建MyDecisionManager类,加上@component 注解,实现AccessDecisionManager类,源码如下:
需要实现该类的三个方法
@Component
public class MyDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> ConfigAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
for (ConfigAttribute configAttribute : ConfigAttributes) {
String needRole = configAttribute.getAttribute(); //在安全框架上下文中,已经添加了角色列表,此处即可获取对应路径的角色
// System.out.println(needRole);
if ("ROLE_LOGIN".equals(needRole)){//如果角色列表返回的是ROLE_LOGIN则表示未登录
if (authentication instanceof AnonymousAuthenticationToken){//判断是否未登录 AnonymousAuthenticationToken 表示匿名
//判断用户是否登陆 根据authentication 是否是匿名的实例 AnonymousAuthenticationToken 为匿名登陆 即未登录
throw new AccessDeniedException("尚未登陆,请登录!");
}else {
// 如果不是匿名 则停止该方法
return;
}
}
// 获取登陆用户的角色 通过authentication.getAuthorities()获取
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (authority.getAuthority().equals(needRole)){//记住 needRole 是通过数据库里面查找到访问该路径的角色(通过MyFilter类查询到的),这里与登陆用户所拥有的角色对比
return;//如果匹配上,即可访问该路径
}
}
}
//如果权限不足
throw new AccessDeniedException("权限不足,请联系管理员!");
}
//如下两个方法返回false即可
@Override
public boolean supports(ConfigAttribute configAttribute) {
return false;
}
@Override
public boolean supports(Class<?> aClass) {
return false;
}
}
到此两个类也配置完了,那么如何加入到权限控制链上呢?
删除原来配置的
添加如下配置
大功告成!
推荐阅读
-
基于Spring Security前后端分离的权限控制系统问题
-
详解基于vue-router的动态权限控制实现方案
-
Vue 动态路由的实现及 Springsecurity 按钮级别的权限控制
-
c#, AOP动态代理实现动态权限控制(一)
-
SpringBoot集成Spring security JWT实现接口权限认证
-
springBoot+security+mybatis 实现用户权限的数据库动态管理
-
Spring Security 前后端分离下的权限控制
-
Springboot + Spring Security 前后端分离权限控制
-
jwt,spring security ,feign,zuul,eureka 前后端分离 整合 实现 简单 权限管理系统 与 用户认证的实现
-
Springboot+Spring Security实现前后端分离登录认证及权限控制的示例代码