签名验证与权限细化
自定义过滤器
有时某些业务或者功能,需要在用户请求到来之前就进行一些判断或执行某些动作,就像在Servlet中的FilterChain
过滤器所做的那样,Spring Security也有类似机制。
Spring Security有三种增加过滤器的方式。
addFilterBefore()
:事前。addFilterAt()
:事中。addFilterAfter()
:事后。
当然,也可以修改过滤器的行为。
http.logout().disable()
、http.headers().disable()
等方式。或者用自定义过滤器
http.addFilterAt(new MyLogoutFilter(), LogoutFilter.class)
替换。
Spring Security官方在这里列出了过滤器调用顺序,对其中的部分过滤器说明如下。
别名 | 过滤器类 | 说明 |
---|---|---|
CHANNEL_FILTER | ChannelProcessingFilter | 访问协议控制,如从http转为https |
SECURITY_CONTEXT_FILTER | SecurityContextPersistenceFilter | 创建SecurityContext,并在请求结束后保存到HttpSession中 |
CONCURRENT_SESSION_FILTER | ConcurrentSessionFilter | 并发访问控制 |
HEADERS_FILTER | HeaderWriterFilter | 增加头部信息 |
CSRF_FILTER | CsrfFilter | 跨域配置 |
LOGOUT_FILTER | LogoutFilter | 登出 |
X509_FILTER | X509AuthenticationFilter | X509验证 |
PRE_AUTH_FILTER | AbstractPreAuthenticatedProcessingFilter | 只使用授权功能,而不使用认证功能 |
CAS_FILTER | CasAuthenticationFilter | 认证相关 |
FORM_LOGIN_FILTER | UsernamePasswordAuthenticationFilter | 认证相关 |
BASIC_AUTH_FILTER | BasicAuthenticationFilter | 认证相关 |
SERVLET_API_SUPPORT_FILTER | SecurityContextHolderAwareRequestFilter | 包装类 |
JAAS_API_SUPPORT_FILTER | JaasApiIntegrationFilter | |
REMEMBER_ME_FILTER | RememberMeAuthenticationFilter | 配置RememberMe |
ANONYMOUS_FILTER | AnonymousAuthenticationFilter | 匿名身份管理 |
SESSION_MANAGEMENT_FILTER | SessionManagementFilter | Session管理 |
EXCEPTION_TRANSLATION_FILTER | ExceptionTranslationFilter | 异常处理 |
FILTER_SECURITY_INTERCEPTOR | FilterSecurityInterceptor | 拿到用户与资源权限并比对 |
SWITCH_USER_FILTER | SwitchUserFilter | 切换用户 |
例如,如果想在登录之前检查确认接口签名的有效性,就可以先自定义拦截过滤器。
package com.xiangwang.vmall.security;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.DigestUtils;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import java.io.IOException;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
/**
* 自定义拦截过滤器
*
*/
@Component
public class CustomInterceptorFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
// 保存参数=参数值对
Map<String, String> mapResult = new HashMap<String, String>();
// 请求中的参数
Enumeration<String> em = request.getParameterNames();
while (em.hasMoreElements()) {
String paramName = em.nextElement();
String value = request.getParameter(paramName);
mapResult.put(paramName, value);
}
// 验证参数,只要有一个不满足条件,立即返回
if (null == mapResult.get("platform") ||
null == mapResult.get("timestamp") ||
null == mapResult.get("signature")) {
response.getWriter().write("api validate failure");
}
Object result = null;
String platform = mapResult.get("platform");
String timestamp = mapResult.get("timestamp");
String signature = mapResult.get("signature");
// 后端生成签名:platform = "xiangwang" timestamp = "159123456789" signature = ""
String sign = DigestUtils.md5DigestAsHex((platform + timestamp).getBytes());
validateSignature(signature, sign, request, response, chain);
}
// 验证签名
private void validateSignature(String signature, String sign, ServletRequest request, ServletResponse response, FilterChain chain) throws IOException {
if (signature.equalsIgnoreCase(sign)) {
try {
// 让调用链继续往下执行
chain.doFilter(request, response);
} catch (Exception e) {
response.getWriter().write("api validate failure");
}
} else {
response.getWriter().write("api validate failure");
}
}
/**
* 下面只是伪代码,举个例子而已
*
*/
public static void main(String[] args) {
// 这里的验证签名算法可以随便自定义实现(guid = "0" platform = "web" timestamp = 156789012345)
//
String sign = "";
if(StringUtils.isBlank(guid)) {
// 首次登录,后端 platform + timestamp + "xiangwang" 生成签名
sign = DigestUtils.md5DigestAsHex((platform + timestamp + "xiangwang").getBytes());
validateSignature(signature, sign, request, response, chain);
} else {
// 不是首次登录,后端 guid + platform + timestamp 生成签名
// 从Redis拿到token,这里不实现
// Object object = service.getObject("token#" + guid);
Object object = "1234567890abcdefghijklmnopqrstuvwxyz";
if(null == object) {
response.getWrite().write("token expired");
} else {
token = (String) obejct;
// 验证sign
sign = DigestUtils.md5DigestAsHex((platform + timestamp + "xiangwang").getBytes());
validateSignature(signature, sign, request, response, chain);
}
}
System.out.println(DigestUtils.md5DigestAsHex(("web" + "156789012345").getBytes()));
}
}
然后修改WebSecurityConfiguration
中的configure()
方法就可以了。
......
// 控制逻辑
@Override
protected void configure(HttpSecurity http) throws Exception {
// 执行UsernamePasswordAuthenticationFilter之前添加拦截过滤
http.addFilterBefore(new CustomInterceptorFilter(), UsernamePasswordAuthenticationFilter.class);
......
}
运行Postman,给login()
接口增加如下参数。

角色权限细化
之前只演示了用户 -> 角色
之间的对应关系,没有考虑权限
,接下来就对它进行细化,为每个角色赋予权限。
首先,创建所需要的数据库表。
DROP TABLE IF EXISTS sys_permission;
CREATE TABLE sys_permission (
id int(11) NOT NULL AUTO_INCREMENT,
roleid int(11) NOT NULL,
path varchar(256) NOT NULL,
permission varchar(64) NOT NULL,
createtime datetime NOT NULL,
updatetime datetime NOT NULL,
PRIMARY KEY (id) USING BTREE
) ENGINE = MyISAM CHARSET = utf8mb4 COMMENT='权限表';
-- 插入几条权限数据
INSERT INTO sys_permission VALUES (1,1,'/admin','create',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);
INSERT INTO sys_permission VALUES (2,1,'/admin','read',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);
INSERT INTO sys_permission VALUES (3,2,'/manager','create',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);
INSERT INTO sys_permission VALUES (4,2,'/manager','remove',CURRENT_TIMESTAMP,CURRENT_TIMESTAMP);
然后,创建对应的实体类。
package com.xiangwang.vmall.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.jdbc.core.RowMapper;
import java.io.Serializable;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Date;
/**
* 权限entity
*
*/
public class SysPermission implements Serializable, RowMapper<SysPermission> {
private static final long serialVersionUID = 4121559180789799491L;
private int id;
private int roleid;
private String path;
private String permission;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
protected Date createtime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
protected Date updatetime;
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getRoleid() {
return roleid;
}
public void setRoleid(int roleid) {
this.roleid = roleid;
}
public String getPath() {
return path;
}
public void setPath(String path) {
this.path = path;
}
public String getPermission() {
return permission;
}
public void setPermission(String permission) {
this.permission = permission;
}
public Date getCreatetime() {
return createtime;
}
public void setCreatetime(Date createtime) {
this.createtime = createtime;
}
public Date getUpdatetime() {
return updatetime;
}
public void setUpdatetime(Date updatetime) {
this.updatetime = updatetime;
}
@Override
public SysPermission mapRow(ResultSet result, int i) throws SQLException {
SysPermission permission = new SysPermission();
permission.setId(result.getInt("id"));
permission.setRoleid(result.getInt("roleid"));
permission.setPath(result.getString("path"));
permission.setPermission(result.getString("permission"));
permission.setCreatetime(result.getTimestamp("createtime"));
permission.setUpdatetime(result.getTimestamp("updatetime"));
return permission;
}
}
接着,创建对应的服务类。
package com.xiangwang.vmall.service;
import com.xiangwang.vmall.dao.MySQLDao;
import com.xiangwang.vmall.entity.SysPermission;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
/**
* 权限Service
*
*/
@Service
public class PermissionService {
@Autowired
private MySQLDao mySQLDao;
// 得到某个角色的全部权限
public List<SysPermission> getByRoleId(int roleid) {
String sql = "SELECT id, url, roleid, permission, createtime, updatetime FROM sys_permission WHERE roleid = ?";
return mySQLDao.find(sql, new SysPermission(), roleid);
}
}
修改LoginController
登录类,在其中增加几个hasPermission()
方法。
......
// 细化权限
@GetMapping("/admin/create")
@PreAuthorize("hasPermission('/admin', 'create')")
public String adminCreate() {
return "admin有ROLE_ADMIN角色的create权限";
}
@GetMapping("/admin/read")
@PreAuthorize("hasPermission('/admin', 'read')")
public String adminRead() {
return "admin有ROLE_ADMIN角色的read权限";
}
@GetMapping("/manager/create")
@PreAuthorize("hasPermission('/manager', 'create')")
public String managerCreate() {
return "manager有ROLE_MANAGER角色的create权限";
}
@GetMapping("/manager/remove")
@PreAuthorize("hasPermission('/manager', 'remove')")
public String managerRemove() {
return "manager有ROLE_MANAGER角色的remove权限";
}
......
因为增加了@PreAuthorize
注解,所以还需要自定义PermissionEvaluator
类实现对hasPermission()
方法的处理。
package com.xiangwang.vmall.security;
import com.xiangwang.vmall.entity.SysPermission;
import com.xiangwang.vmall.service.PermissionService;
import com.xiangwang.vmall.service.RoleService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.Collection;
import java.util.List;
/**
* 自定义权限处理
*
*/
@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {
@Autowired
private RoleService roleService;
@Autowired
private PermissionService permissionService;
@Override
public boolean hasPermission(Authentication authentication, Object targetUrl, Object permission) {
// 获得loadUserByUsername()方法的结果
User user = (User) authentication.getPrincipal();
// 获得用户授权
Collection<GrantedAuthority> authorities = user.getAuthorities();
// 遍历用户所有角色
for(GrantedAuthority authority : authorities) {
String roleName = authority.getAuthority();
int roleid = roleService.getByName(roleName).getId();
// 得到角色所有的权限
List<SysPermission> permissionList = permissionService.getByRoleId(roleid);
if (null == permissionList) {
continue;
}
// 遍历permissionList
for(SysPermission sysPermission : permissionList) {
String pstr = sysPermission.getPermission();
String path = sysPermission.getPath();
// 判空
if (StringUtils.isBlank(pstr) || StringUtils.isBlank(path)) {
continue;
}
// 如果访问的url和权限相符,返回true
if (path.equals(targetUrl) && pstr.equals(permission)) {
return true;
}
}
}
return false;
}
@Override
public boolean hasPermission(Authentication authentication, Serializable serializable, String targetUrl, Object permission) {
return false;
}
}
最后,依然是修改WebSecurityConfiguration
,在过滤器链中加入自定义的权限识别类CustomPermissionEvaluator
。
......
@Autowired
private CustomPermissionEvaluator permissionEvaluator;
......
// 注入自定义PermissionEvaluator
@Bean
public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
handler.setPermissionEvaluator(permissionEvaluator);
return handler;
}
......
启动服务之前,要在配置文件中加入下面的这一行。
spring.main.allow-bean-definition-overriding=true
然后用Postman访问接口。
通过
admin
用户登录,分别做以下测试。接口 访问结果 /admin/create 成功 /admin/read 成功 /manager/create 失败 /manager/remove 失败 通过
manager
用户登录,分别做以下测试。接口 访问结果 /admin/create 失败 /admin/read 失败 /manager/create 成功 /manager/remove 成功
以上就实现了对于角色的权限细化。
感谢支持
更多内容,请移步《超级个体》。