SSM整合SpringSecurity简单使用
一、SpringSecurity
1.1 什么是SpringSecurity
Spring Security 的前身是 Acegi Security ,是 Spring 项目组中用来提供安全认证服务的框架。(官网地址)
Spring Security 为基于J2EE企业应用软件提供了全面安全服务。特别是使用领先的J2EE解决方案-Spring框架开发的企业软件项目。人们使用Spring Security有很多种原因,不过通常吸引他们的是在J2EE Servlet规范或EJB规范中找不到典型企业应用场景的解决方案。特别要指出的是他们不能再
WAR 或 EAR 级别进行移植。这样,如果你更换服务器环境,就要,在新的目标环境进行大量的工作,对你的应用
系统进行重新配置安全。使用Spring Security 解决了这些问题,也为你提供很多有用的,完全可以指定的其他安
全特性。安全包括两个主要操作。
- “认证”,是为用户建立一个他所声明的主体。主体一般是指用户,设备或可以在你系统中执行动作的其他系
统。(可以将主体当前权限框架自己的session,认证其实就是登录操作,并将登录成功的数据信息存入主体)
- “授权”,指的是一个用户能否在你的应用中执行某个操作,在到达授权判断之前,身份的主题已经由身份验证
过程建立了。(查询是否对应权限,授权其实就是在认证之后请求需要权限的资源时,查询数据库在主体中保存对应权限数据)
这些概念是通用的,不是Spring Security特有的。在身份验证层面,Spring Security广泛支持各种身份验证模式,
这些验证模型绝大多数都由第三方提供,或则正在开发的有关标准机构提供的,例如 Internet Engineering Task
Force.作为补充,Spring Security 也提供了自己的一套验证功能。
Spring Security 目前支持认证一体化如下认证技术:
HTTP BASIC authentication headers (一个基于IEFT RFC 的标准)
HTTP Digest authentication headers (一个基于IEFT RFC 的标准)
HTTP X.509 client certi?cate exchange(一个基于IEFT RFC 的标准)
LDAP (一个非常常见的跨平台认证需要做法,特别是在大环境)
Form-based-authentication (提供简单用户接口的需求)
OpenID authentication Computer Associates Siteminder JA-SIG Central Authentication Service (CAS,这是一个流行的开源单点登录系统)
Transparent authentication context propagation for Remote Method Invocation and HttpInvoker (一个Spring远程调用协议)
1.2 核心组件
SecurityContextHolder
SecurityContextHolder它持有的是安全上下文 (security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权等等,这些都被保存在SecurityContextHolder中。SecurityContextHolder默认使用ThreadLocal 策略来存储认证信息。看到 ThreadLocal 也就意味着,这是一种与线程绑定的策略。在web环境下,Spring Security在用户登录时自动绑定认证信息到当前线程,在用户退出时,自动清除当前线程的认证信息
看源码他有静态方法
//获取 上下文public static SecurityContext getContext() {return strategy.getContext();}//清除上下文 public static void clearContext() {strategy.clearContext();}
SecurityContextHolder.getContext().getAuthentication().getPrincipal()
getAuthentication() 返回了认证信息,
getPrincipal() 返回了身份信息
UserDetails 便是Spring对身份信息封装的一个接口
SecurityContext
安全上下文,主要持有 Authentication 对象,如果用户未鉴权,那Authentication对象将会是空的。看源码可知
package org.springframework.security.core.context;import java.io.Serializable;
import org.springframework.security.core.Authentication;public interface SecurityContext extends Serializable {Authentication getAuthentication();void setAuthentication(Authentication var1);
}
Authentication
鉴权对象,该对象主要包含了用户的详细信息 (UserDetails) 和用户鉴权时所需要的信息,如用户提交的用户名密码、Remember-me Token,或者digest hash值等,按不同鉴权方式使用不同的 Authentication 实现,看源码可知道
package org.springframework.security.core;import java.io.Serializable;
import java.security.Principal;
import java.util.Collection;public interface Authentication extends Principal, Serializable {Collection<? extends GrantedAuthority> getAuthorities();Object getCredentials();Object getDetails();Object getPrincipal();boolean isAuthenticated();void setAuthenticated(boolean var1) throws IllegalArgumentException;}
注意 GrantedAuthority 该接口表示了当前用户所拥有的权限(或者角色)信息。这些信息由授权负责对象 AccessDecisionManager 来使用,并决定最终用户是否可以访问某 资源(URL或方法调用或域对象)。鉴权时并不会使用到该对象
UserDetails
这个接口规范了用户详细信息所拥有的字段,譬如用户名、密码、账号是否过期、是否锁定等。在Spring Security中,获取当前登录的用户的信息,一般情况是需要在这个接口上面进行 扩展,用来对接自己系统的用户,看源码可知
package org.springframework.security.core.userdetails;import java.io.Serializable;
import java.util.Collection;
import org.springframework.security.core.GrantedAuthority;public interface UserDetails extends Serializable {Collection<? extends GrantedAuthority> getAuthorities();String getPassword();String getUsername();boolean isAccountNonExpired();boolean isAccountNonLocked();boolean isCredentialsNonExpired();boolean isEnabled();
}
UserDetailsService
这个接口只提供一个接口 loadUserByUsername(String username) ,这个接口非常 重要, 一般情况我们都是通过 扩展 这个接口来显示获取我们的用户信息,用户登陆时传递的用户名和密码也是通过这里这查找出来的用户名和密码进行校验,但是真正的校验不在这里,而是由 AuthenticationManager 以及 AuthenticationProvider 负责的,需要强调的是,如果用户不存在,不应返回 NULL,而要抛出异常 UsernameNotFoundException,看源码可知
package org.springframework.security.core.userdetails;public interface UserDetailsService {UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
1.3 SpringSecurity快速入门
创建测试web工程
导入依赖
<dependencies><!--springMVC--><dependency><groupId>org.springframework</groupId><artifactId>spring-webmvc</artifactId><version>5.2.9.RELEASE</version></dependency><!--servlet--><dependency><groupId>javax.servlet</groupId><artifactId>javax.servlet-api</artifactId><version>4.0.1</version><scope>provided</scope></dependency><!--jsp--><dependency><groupId>javax.servlet</groupId><artifactId>jsp-api</artifactId><version>2.0</version><scope>provided</scope></dependency><!--jstl--><dependency><groupId>jstl</groupId><artifactId>jstl</artifactId><version>1.2</version></dependency><!--jsckson --><dependency><groupId>com.fasterxml.jackson.core</groupId><artifactId>jackson-databind</artifactId><version>2.9.9</version></dependency><!--文件上传--><dependency><groupId>commons-fileupload</groupId><artifactId>commons-fileupload</artifactId><version>1.4</version></dependency><!--spring相关坐标--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.2.9.RELEASE</version></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.5</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>5.2.9.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-test</artifactId><version>5.2.9.RELEASE</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-web</artifactId><version>5.2.9.RELEASE</version></dependency><!--mybatis相关坐标--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>5.1.47</version></dependency><!--数据库连接池--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.1.15</version></dependency><!--mybatis--><dependency><groupId>org.mybatis</groupId><artifactId>mybatis</artifactId><version>3.5.6</version></dependency><!--spring-mybatis坐标--><dependency><groupId>org.mybatis</groupId><artifactId>mybatis-spring</artifactId><version>1.3.2</version></dependency><!--lombok--><dependency><groupId>org.projectlombok</groupId><artifactId>lombok</artifactId><version>1.18.10</version></dependency><!--分页插件--><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper</artifactId><version>5.1.10</version></dependency><!--junit单元测试--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.12</version></dependency><!--spring-security--><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-web</artifactId><version>5.2.9.RELEASE</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId><version>5.2.9.RELEASE</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-core</artifactId><version>5.2.9.RELEASE</version></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-taglibs</artifactId><version>5.2.9.RELEASE</version></dependency><dependency><groupId>javax.annotation</groupId><artifactId>jsr250-api</artifactId><version>1.0</version></dependency><!--log4j依赖--><dependency><groupId>log4j</groupId><artifactId>log4j</artifactId><version>1.2.17</version></dependency></dependencies>
web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://java.sun.com/xml/ns/javaee"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://java.sun.com/xml/ns/javaeehttp://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"version="3.0"><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring-security.xml</param-value></context-param><listener><listener-class>org.springframework.web.context.ContextLoaderListener</listener-class></listener><filter><filter-name>springSecurityFilterChain</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class></filter><filter-mapping><filter-name>springSecurityFilterChain</filter-name><url-pattern>/*</url-pattern></filter-mapping><welcome-file-list><welcome-file>index.html</welcome-file><welcome-file>index.htm</welcome-file><welcome-file>index.jsp</welcome-file><welcome-file>default.html</welcome-file><welcome-file>default.htm</welcome-file><welcome-file>default.jsp</welcome-file></welcome-file-list>
</web-app>
spring-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:security="http://www.springframework.org/schema/security"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security.xsd"><security:http auto-config="true" use-expressions="false" ><!--intercept-url定义一个过滤规则pattern表示对哪些url进行权限控制,ccess属性表示在请求对应的URL时需要什么权限,默认配置时它应该是一个以逗号分隔的角色列表,请求的用户只需拥有其中的一个角色就能成功访问对应的URL--><security:intercept-url pattern="/**" access="ROLE_USER" /><!--<security:form-login />--></security:http><security:authentication-manager><security:authentication-provider><security:user-service><security:user name="user" password="{noop}user"authorities="ROLE_USER" /><security:user name="admin" password="{noop}admin"authorities="ROLE_ADMIN" /></security:user-service></security:authentication-provider></security:authentication-manager>
</beans>
测试
1.4 SpringSecurity 使用自定义页面
添加以下页面
修改SpringSecurity.xml配置文件
<!-- 配置不过滤的资源(静态资源及登录相关) -->
<security:http security="none" pattern="/login.html" />
<security:http security="none" pattern="/failer.html" />
<security:http auto-config="true" use-expressions="false" ><!-- 配置资料连接,表示任意路径都需要ROLE_USER权限 --><security:intercept-url pattern="/**" access="ROLE_USER" /><!-- 自定义登陆页面,login-page 自定义登陆页面 authentication-failure-url 用户权限校验失败之后才会跳转到这个页面,如果数据库中没有这个用户则不会跳转到这个页面。default-target-url 登陆成功后跳转的页面。 注:登陆页面用户名固定 username,密码 password,action:login --><security:form-login login-page="/login.html"login-processing-url="/login" username-parameter="username"password-parameter="password" authentication-failure-url="/failer.html"default-target-url="/success.html" authentication-success-forward-url="/success.html"/><!-- 关闭CSRF,默认是开启的 --><security:csrf disabled="true" />
</security:http>
<security:authentication-manager><security:authentication-provider><security:user-service><security:user name="user" password="{noop}user"authorities="ROLE_USER" /><security:user name="admin" password="{noop}admin"authorities="ROLE_ADMIN" /></security:user-service></security:authentication-provider></security:authentication-manager>
二、 用户登录
2.1 表结构分析与创建
-- 用户表
CREATE TABLE users(id VARCHAR(32) PRIMARY KEY,email VARCHAR(50) UNIQUE NOT NULL,username VARCHAR(50),PASSWORD VARCHAR(100),phoneNum VARCHAR(20),STATUS INT
);-- 角色表
CREATE TABLE role(id VARCHAR(32) PRIMARY KEY,roleName VARCHAR(50) ,roleDesc VARCHAR(50)
);-- 用户角色关联表
CREATE TABLE users_role(userId VARCHAR(32),roleId VARCHAR(32),PRIMARY KEY(userId,roleId),FOREIGN KEY (userId) REFERENCES users(id),FOREIGN KEY (roleId) REFERENCES role(id)
);-- 资源权限表
CREATE TABLE permission(id VARCHAR(32) PRIMARY KEY,permissionName VARCHAR(50) ,url VARCHAR(50)
);-- 角色权限关联表
CREATE TABLE role_permission(permissionId VARCHAR(32),roleId VARCHAR(32),PRIMARY KEY(permissionId,roleId),FOREIGN KEY (permissionId) REFERENCES permission(id),FOREIGN KEY (roleId) REFERENCES role(id)
);
2.2 创建实体类
创建Users
@Data
@NoArgsConstructor
public class Users{private int id;private String email;private String username;private String password;private String phoneNum;private int status;// '状态0 未开启 1 开启'private String statusStr;private Role role;public String getStatusStr() {if(status==0){statusStr="未开启";}else if(status==1){statusStr="开启";}return statusStr;}
}
创建Role
@Data
@NoArgsConstructor
public class Role {private int id;private String roleName;private String roleDesc;private List<Permission> permissions;
}
创建Permission
@Data
@NoArgsConstructor
public class Permission {private int id;private String permissionName;private String url;
}
2.3 Spring Security使用数据库认证
在Spring Security中如果想要使用数据进行认证操作,有很多种操作方式,这里我们介绍使用UserDetails、
UserDetailsService来完成操作。
-
UserDetails
public interface UserDetails extends Serializable {Collection<? extends GrantedAuthority> getAuthorities();String getPassword();String getUsername();boolean isAccountNonExpired();boolean isAccountNonLocked();boolean isCredentialsNonExpired();boolean isEnabled(); }
UserDetails是一个接口,我们可以认为UserDetails作用是于封装当前进行认证的用户信息,但由于其是一个
接口,所以我们可以对其进行实现,也可以使用Spring Security提供的一个UserDetails的实现类User来完成
操作
以下是User类的部分代码
public class User implements UserDetails, CredentialsContainer {private String password;private final String username;private final Set<GrantedAuthority> authorities;private final boolean accountNonExpired; //帐户是否过期private final boolean accountNonLocked; //帐户是否锁定private final boolean credentialsNonExpired; //认证是否过期 private final boolean enabled; //帐户是否可用 }
-
UserDetailsService
public interface UserDetailsService {UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
上面将UserDetails与UserDetailsService做了一个简单的介绍,那么我们具体如何完成Spring Security的数据库认证操作哪,我们通过用户管理中用户登录来完成Spring Security的认证操作。
2.4 用户登录流程分析
2.5 代码编写
编写login和failer页面
导入spring security相关坐标
<!--spring-security-->
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-web</artifactId><version>5.2.9.RELEASE</version>
</dependency>
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-config</artifactId><version>5.2.9.RELEASE</version>
</dependency>
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-core</artifactId><version>5.2.9.RELEASE</version>
</dependency>
<dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-taglibs</artifactId><version>5.2.9.RELEASE</version>
</dependency>
配置web.xml
<!--加载spring环境 和 spring-security配置文件--><context-param><param-name>contextConfigLocation</param-name><param-value>classpath:spring/applicationContext*.xml,classpath:spring/spring-security.xml</param-value></context-param><!-- 额外添加springSecurity过滤器 -->
<filter><filter-name>springSecurityFilterChain</filter-name><filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping><filter-name>springSecurityFilterChain</filter-name><url-pattern>/*</url-pattern>
</filter-mapping>
配置spring-security.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:security="http://www.springframework.org/schema/security"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beanshttp://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/securityhttp://www.springframework.org/schema/security/spring-security.xsd"><!-- 配置不拦截的页面与静态资源 --><security:http pattern="/login.jsp" security="none"/><security:http pattern="/failer.jsp" security="none"/><security:http pattern="/css/**" security="none"/><security:http pattern="/img/**" security="none"/><security:http pattern="/layui/**" security="none"/><!--配置具体的规则auto-config="true" 不用自己编写登录的页面,框架提供默认登录页面use-expressions="false" 是否使用SPEL表达式--><security:http auto-config="true" use-expressions="false"><!-- 同源策略 如果页面使用iframe需要配置 否则不能使用 --><security:headers><security:frame-options disabled="true"/></security:headers><!-- 配置具体的拦截的规则 pattern="请求路径的规则" access="访问系统的人,必须有ROLE_USER的角色" --><security:intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN" /><!-- 定义跳转的具体的页面 --><security:form-loginlogin-page="/login.jsp"login-processing-url="/login"default-target-url="/index.jsp"authentication-failure-url="/failer.jsp"authentication-success-forward-url="/index.jsp"/><!-- 关闭跨域请求 --><security:csrf disabled="true"/><!-- 退出 --><security:logout invalidate-session="true" logout-url="/logout" logout-success-url="/login.jsp" /></security:http><!-- 切换成数据库中的用户名和密码 --><security:authentication-manager><!-- user-service-ref="userService" springSecurity用于获取账号信息的类 需要实现UserDetailsService 重写加载数据方法 --><security:authentication-provider user-service-ref="userService"><!-- 配置加密的方式 注意如果使用需要开启下面配置的加密类<security:password-encoder ref="passwordEncoder"/>--></security:authentication-provider></security:authentication-manager><!-- 配置加密类 --><!--<bean id="passwordEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/>--><!-- 提供了入门的方式,在内存中存入用户名和密码<security:authentication-manager><security:authentication-provider><security:user-service><security:user name="admin" password="{noop}admin" authorities="ROLE_USER"/></security:user-service></security:authentication-provider></security:authentication-manager>--></beans>
spring security接收到用户名之后,spring security怎么就知道我们要调用的是那个service来完成用户的查询操作呢?
在配置文件中有这么一段配置:
<security:authentication-manager><security:authentication-provider user-service-ref="userService"><!-- 配置加密的方式<security:password-encoder ref="passwordEncoder"/>--></security:authentication-provider>
</security:authentication-manager>
user-service-ref
属性就是来指定要执行的service,当然这个service来继承 UserDetailsService,扩展我们自己的service接口,完成用户的认证的操作
编写UserMapper
public interface UserMapper {//使用权限框架操作 根据账号查询信息@Select("select * from users where username=#{username} and status=1")public Users selectByUserName(String username);}
编写UserService
public interface UserService extends UserDetailsService {
}
@Service("userService")
public class UserServiceImpl implements UserService {@AutowiredUserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//查询数据返回实体类对应数据Users users = userMapper.selectByUserName(username);if(users!=null){//创建springSecurity主体中存储的账号对象并返回(账号,密码,权限列表)//权限列表这里填null注意UserDetails userDetails=new User(users.getUsername(),users.getPassword(),null);return userDetails;}return null;}}
测试
在登录页面输入用户名 密码 跳转到了错误页面:
三、 用户登录问题分析
问题一:登录失败的问题
在数据库中是有jack这个用户的
为什么还是没办法登录,原因是在于我们的spring-security.xml配置文件中配置的问题:
在配置文件中,我们配置了
<!-- 配置具体的拦截的规则 pattern="请求路径的规则" access="访问系统的人,必须有ROLE_USER的角色" -->
<security:intercept-url pattern="/**" access="ROLE_USER,ROLE_ADMIN"/>
spring security 拦截了所有的请求,要想登录认证必须是有ROLE_USER,ROLE_ADMIN
两个权限的,在UserServiceImpl类中我将查询到的UserInfo
中的信息封装到了User
对象中,有一个值是为null
User user = new User(userInfo.getUsername(),userInfo.getPassword(), null);
也就是说我们查询出的这个对象是没有权限的,既是用户名和密码对了,没有权限也是没有办法登录的!
在这里我们需要模拟给出ROLE_USER,ROLE_ADMIN
两个权限,修改UserServiceImpl类
@Service("userService")
public class UserServiceImpl implements UserService {@AutowiredUserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//查询数据返回实体类对应数据Users users = userMapper.selectByUserName(username);if(users!=null){//创建springSecurity主体中存储的账号对象并返回(账号,密码,权限列表)UserDetails userDetails=new User(users.getUsername(),users.getPassword(),getAuthority());return userDetails;}return null;}public List<SimpleGrantedAuthority> getAuthority(){ArrayList<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_USER"));simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));return simpleGrantedAuthorities;}
}
修改完毕之后我们再次测试,看是否能登录成功!
这个报错的原因是spring security 默认使用的是密文提交的,现在没有进行加密! 我们只需要在获取密码的前面加上"{noop}" 代表的是使用明文
此时就认证成功
问题二:数据库查询用户角色
在上面的代码中我们的用户角色是自己手动添加的,这不是通用的一种方式,用户的角色我们要从数据库中进行查询!
修改UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="cn.yanqi.ssm.mapper.UserMapper"><!--通过用户名查询用户--><resultMap id="userResultMap" type="UserInfo" autoMapping="true"><id property="id" column="id" /><collection property="roles" ofType="Role" javaType="List" autoMapping="true"><id property="id" column="rid" /></collection></resultMap><select id="findUserByUserName" resultMap="userResultMap">SELECT* ,r.id ridFROMusers u,role r,users_role urWHEREu.id = ur.userId ANDr.id = ur.roleId ANDu.username = #{username}</select></mapper>
修改UserService
在这里需要注意的是,获取到对应的角色之后,其实还是不能登录,在users表中,有一个字段status
,这个字段表示的用户是否可能,0表示可用 1表示不可用。此时的功能原来使用的构造已经无法满足了,我们需要使用另外一个构造
public User(String username, String password,/**boolean enabled: 账户是否可用boolean accountNonExpired:账户是否过期boolean credentialsNonExpired:密码是否过期boolean accountNonLocked:账户是否冻结被锁定authorities:该账户所具有的权限*/boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection<? extends GrantedAuthority> authorities) {}
完整代码实现
@Service("userService")
public class UserServiceImpl implements UserService {@Autowiredprivate UserMapper userMapper;@Overridepublic UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {UserInfo userInfo = userMapper.findUserByUserName(s);User user = new User(userInfo.getUsername(),"{noop}"+userInfo.getPassword(),userInfo.getStatus() == 0 ? false : true ,true,true,true,getAuthority(userInfo.getRoles()));return user;}public List<SimpleGrantedAuthority> getAuthority(List<Role> roles){ArrayList<SimpleGrantedAuthority> simpleGrantedAuthorities = new ArrayList<>();for (Role role : roles) {simpleGrantedAuthorities.add(new SimpleGrantedAuthority("ROLE_"+role.getRoleName()));}return simpleGrantedAuthorities;}
}
四、动态权限
在实际开发中一般通过对数据库表的操作实现动态的权限操作,SpringSecurity实现动态权限比较复杂需要自己实现过滤器与决策器,所以这里的内容是基于权限管理crud功能实现后的配置,没有进行详细的讲解,如需详细了解可以查看Spring Security认证与授权的原理
4.1 将公开权限设置为无需认证即可访问
<!-- 配置不拦截的资源 --><security:http pattern="/login.jsp" security="none"/><security:http pattern="/failer.jsp" security="none"/><security:http pattern="/css/**" security="none"/><security:http pattern="/img/**" security="none"/><security:http pattern="/layui/**" security="none"/>
4.2 配置具体的规则
<!--配置具体的规则auto-config="true" 不用自己编写登录的页面,框架提供默认登录页面use-expressions="true" 是否使用SPEL表达式(否则只能使用USER_角色的形式配置)--><security:http auto-config="true" use-expressions="true"><!-- 同源策略 如果页面使用iframe需要配置 否则不能使用 --><security:headers><security:frame-options disabled="true"/></security:headers><!-- 定义跳转的具体的页面 --><security:form-loginlogin-page="/login.jsp"login-processing-url="/login"default-target-url="/index.jsp"authentication-failure-url="/failer.jsp"authentication-success-forward-url="/users/name"/><security:intercept-url pattern="/**" access="isAuthenticated()"/><!-- 配置具体的拦截的规则 pattern="请求路径的规则" access="isAuthenticated()" <!-- 权限框架本质是过滤器链 会依次进行权限验证 如果验证通过继续执行 该配置 配置的是 除以上不拦截的资源外 所有url请求必须拥有认证的权限(登录后才能访问)
--><!-- 关闭跨域请求 --><security:csrf disabled="true"/><!-- 退出 --><security:logout invalidate-session="true" logout-url="/logout" logout-success-url="/login.jsp" /></security:http>
2.3 设置权限数据
4.4 自定义相关过滤器与决策器
自定义决策管理器
权限框架本身是由多个不同功能的过滤器组成的,不同的过滤器负责不同的功能例如认证过滤、静态资源过滤、等 授权过滤也是一样,决策器就是同于判断当前请求的url当前账号是否拥有权限
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 为MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法返回的结果,此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。@Overridepublic 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");}@Overridepublic boolean supports(ConfigAttribute attribute) {return true;}@Overridepublic boolean supports(Class<?> clazz) {return true;}
}
自定义权限加载器
import com.yunhe.javabean.Permission;
import com.yunhe.mapper.PermissionMapper;
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 javax.servlet.http.HttpServletRequest;
import java.util.*;@Service
//授权管理器
//用于当前项目要动态配置的权限信息
//从数据库中取出所有的权限信息 进行配置
//这样当客户请求对应权限时进行权限验证(因为不可能将所有的url都过滤)
public class MyInvocationSecurityMetadataSourceService implementsFilterInvocationSecurityMetadataSource {@Autowired//注入权限查询的dao层private PermissionMapper permissionMapper;private HashMap<String, Collection<ConfigAttribute>> map =null;/*** 加载权限表中所有权限*/public void loadResourceDefine(){map = new HashMap<>();Collection<ConfigAttribute> array;ConfigAttribute cfg;//动态查询当前数据库中所有的权限List<Permission> permissions = permissionMapper.selectAll();for(Permission permission : permissions) {array = new ArrayList<>();cfg = new SecurityConfig(permission.getPermissionName());//此处只添加了权限的名字,其实还可以添加更多权限的信息,例如请求方法到ConfigAttribute的集合中去。此处添加的信息将会作为MyAccessDecisionManager类的decide的第三个参数。array.add(cfg);//用权限的getUrl() 作为map的key,用ConfigAttribute的集合作为 value,//实际加载存储的结构为 url->[name1,name2....]//url是进行请求url拦截使用的 name是进行权限验证使用的//也就是说在用户进行授权时 实际加载的是权限名称map.put(permission.getUrl(), array);}}//此方法是为了判定用户请求的url 是否在权限表中,如果在权限表中,则返回给 decide 方法,用来判定用户是否有此权限。如果不在权限表中则放行。@Overridepublic Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {if(map ==null) loadResourceDefine();//object 中包含用户请求的request 信息HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();AntPathRequestMatcher matcher;String resUrl;for(Iterator<String> iter = map.keySet().iterator(); iter.hasNext(); ) {resUrl = iter.next();matcher = new AntPathRequestMatcher(resUrl);if(matcher.matches(request)) {return map.get(resUrl);}}return null;}@Overridepublic Collection<ConfigAttribute> getAllConfigAttributes() {return null;}@Overridepublic boolean supports(Class<?> clazz) {return true;}
}
自定义权限拦截器
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.Service;import javax.servlet.*;
import java.io.IOException;@Service
//自定义权限拦截器
//FilterSecurityInterceptor是权限框架中用于处理权限验证的过滤器
public class MyFilterSecurityInterceptor extends AbstractSecurityInterceptor implements Filter {//使用自己定义权限加载器@Autowiredprivate FilterInvocationSecurityMetadataSource securityMetadataSource;//使用自定义的决策管理器@Autowiredpublic void setMyAccessDecisionManager(MyAccessDecisionManager myAccessDecisionManager) {super.setAccessDecisionManager(myAccessDecisionManager);}@Overridepublic void init(FilterConfig filterConfig) throws ServletException {}@Overridepublic void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {FilterInvocation fi = new FilterInvocation(request, response, chain);invoke(fi);}public void invoke(FilterInvocation fi) throws IOException, ServletException {
//fi里面有一个被拦截的url
//里面调用MyInvocationSecurityMetadataSource的getAttributes(Object object)这个方法获取fi对应的所有权限
//再调用MyAccessDecisionManager的decide方法来校验用户的权限是否足够InterceptorStatusToken token = super.beforeInvocation(fi);try {
//执行下一个拦截器fi.getChain().doFilter(fi.getRequest(), fi.getResponse());} finally {super.afterInvocation(token, null);}}@Overridepublic void destroy() {}@Overridepublic Class<?> getSecureObjectClass() {return FilterInvocation.class;}@Overridepublic SecurityMetadataSource obtainSecurityMetadataSource() {return this.securityMetadataSource;}
}
4.5 将自定义权限拦截器配置入权限框架中
在security:http标签中添加
<!-- 配置自定义拦截器 将定义拦截器配置到当前权限框架拦截器链中的指定位置将其配置在本身的权限认证过滤器之后执行-->
<security:custom-filter ref="myFilterSecurityInterceptor" after="FILTER_SECURITY_INTERCEPTOR"></security:custom-filter>
4.6 配置授权页面
在配置后使用账号登录会出现没有权限403代码页面
如果用户登录进行操作时,直接报403错误用户体检并不好,我们可以制作一个比较好看的页面,告诉用户您用户不足,请联系管理员!
在web.xml中配置
<error-page><error-code>403</error-code><location>/failer.jsp</location></error-page>
4.7 授权
在书写权限加载器时,我们发现,加载器加载url是用于http请求的过滤匹配,而实际进行权限验证使用的是权限名称,为了方便书写,我们在登录认证时查询权限信息进行授权操作
修改PermissionMapper
//根据用户id查询权限数据public List<Permission> selectByUid(int uid);
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapperPUBLIC "-//mybatis.org//DTD Mapper 3.0//EN""http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.yunhe.mapper.PermissionMapper"><select id="selectByUid" resultType="com.yunhe.javabean.Permission">select p.*from users u,role r,permission p,users_role ur,role_permission rpwhere u.id=ur.userId and r.id=ur.roleId and r.id=rp.roleId and p.id =rp.permissionId and u.id=#{uid}</select>
</mapper>
修改PermissionService
//根据uid查询权限数据public List<Permission> findByUid(int uid);
@Overridepublic List<Permission> findByUid(int uid) {return permissionMapper.selectByUid(uid);}
修改userService
将我们之前书写写死的角色认证与权限认证查询数据库的形式添加
@Overridepublic UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {//查询数据返回实体类对应数据Users users = userMapper.selectByUserName(username);if (users != null) {//创建springSecurity主体中存储的账号对象并返回(账号,密码,权限列表)UserDetails userDetails = new User(users.getUsername(), users.getPassword(), getAuthority(users.getId()));return userDetails;}return null;}@AutowiredPermissionService permissionService;//获取权限列表public List<GrantedAuthority> getAuthority(int uid) {List<GrantedAuthority> grantedAuthorities = new ArrayList<>();//查询指定用户权限列表List<Permission> permissionList = permissionService.findByUid(uid);for (Permission p:permissionList ) {GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(p.getPermissionName());grantedAuthorities.add(grantedAuthority);}return grantedAuthorities;}
相关文章:
SSM整合SpringSecurity简单使用
一、SpringSecurity 1.1 什么是SpringSecurity Spring Security 的前身是 Acegi Security ,是 Spring 项目组中用来提供安全认证服务的框架。(官网地址) Spring Security 为基于J2EE企业应用软件提供了全面安全服务。特别是使用领先的J2EE解决方案-Spring框架开发…...
Java零基础教程——数据类型
目录数据类型数据类型的分类运算符算术运算符符号做连接符的识别自增、自减运算符赋值运算符关系运算符逻辑运算符短路逻辑运算符三元运算符运算符优先级数据类型 数据类型的分类 引用数据类型(除基本数据类型之外的,如String ) 基本数据类…...
【Linux 信号】信号的产生方式、信号的捕捉的全过程
信号的处理方式是远远比信号的产生当闹钟响了就知道时间到了,说明对应信号处理方法比信号产生更早操作系统的信号处理方法在编写操作系统的时候就已经编写好了signal函数1.1所有的信号1.2 signal函数的概念和简单使用捕捉信号就是自定义对应的信号的处理方法9号信号…...
代码随想录第58天(动态规划):● 392.判断子序列 ● 115.不同的子序列
一、判断子序列 题目描述: 思路和想法: 这道题目还是最长公共子序列的拓展,只是这里进行删除的一定是t字符串,当不相等时,dp[i][j] dp[i][j - 1];其余基本一致。当最长公共子序列个数等s.size()时&#x…...
代码随想录第55天(动态规划):● 309.最佳买卖股票时机含冷冻期 ● 714.买卖股票的最佳时机含手续费
一、最佳买卖股票时机含冷冻期 题目描述: 思路和想法: 这道题相较于之前的题目,注重对状态的分析,这里分为四个状态。 (1)状态一,买入状态 dp[i][0] 操作一:前一天就是持有状态(状…...
字符串装换整数(atoi)-力扣8-java
一、题目描述请你来实现一个 myAtoi(string s) 函数,使其能将字符串转换成一个 32 位有符号整数(类似 C/C 中的 atoi 函数)。函数 myAtoi(string s) 的算法如下:读入字符串并丢弃无用的前导空格检查下一个字符(假设还未…...
毕业5年,从月薪3000到年薪40w,我掌握了那些核心技能?(建议收藏)
大家好,我是静静~~是一枚一线大厂的测试开发工程师很多读者私信问我,自己时间不短了,随着工作年限的不断增长,感觉自己的技术水平与自己的工作年限严重不符。想跳槽出去换个新环境吧,又感觉自己的能力达不到心仪公司的…...
C++中的并行与并发
1.1 并行基础std::thread 用于创建一个执行的线程实例,所以它是一切并发编程的基础,使用时需要包含 <thread> 头文件, 它提供了很多基本的线程操作,例如 get_id() 来获取所创建线程的线程 ID,使用 join() 来加入…...
h2database源码解析-如何更新一条行记录
这里的更新包括两种操作:删、改。更新操作涉及的内容在其他文章里面已经做过介绍了,本文主要是介绍更新的代码流程,以了解更新操作都做了哪些事情。如果有未介绍过的知识点会详细介绍。 目录改(update)如何判读是否加了行锁删(delete)改(upda…...
FyListen——生命周期监听器(设计原理之理解生命周期)
FyListen——生命周期监听器(设计原理之理解生命周期) FyListen 的核心原理有两个: 通过子Fragment对Activity、Fragment进行生命周期监听Java8 接口特性 default 1. 什么是上下文Context 这是一个装饰器模式, ContextImpl 是 …...
Element UI框架学习篇(六)
Element UI框架学习篇(六) 1 删除数据 1.1 前台核心函数 1.1.1 elementUI中的消息提示框语法 //①其中type类型和el-button中的type类型是一致的,有info灰色,success绿色,danger红色,warning黄色,primary蓝色 //②message是你所要填写的提示信息 //③建议都用,因为比双引号…...
Python如何安装模块,python模块安装失败的原因以及解决办法
前言 今天来给刚开始学习python的朋友讲解一下 如何安装python模块, python模块安装失败的原因以及解决办法 很多朋友拿到代码之后,就开始复制粘贴 --> 然后右键进行运行 结果就是报错说 没有这个模块 得安装啥的 Python模块安装 一. 打开命令提示符 win …...
《NFL橄榄球》:洛杉矶闪电·橄榄1号位
洛杉矶闪电(英语:Los Angeles Chargers),又译“洛杉磯衝鋒者”。是一支位于加利福尼亚州洛杉矶郡英格尔伍德的职业美式橄榄球球队,现为美国橄榄球联合会西区成员之一。该队曾于1961年搬迁到圣地亚哥而改叫圣地亚哥电光…...
4.7 Python设置代码格式
随着你编写的程序越来越长,有必要了解一些代码格式设置约定。请花时让你的代码尽可能易于阅读;让代码易于阅读有助于你掌握程序是做什么的,也可以帮助他人理解你编写的代码。为确保所有人编写的代码的结构都大致一致,Python程序员都遵循一些格…...
Zabbix 构建监控告警平台(五)
Zabbix 自动发现Zabbix 自动注册1.Zabbix 自动发现 1.1前言 为了满足监控企业成千上万台服务器,因此我们需要使用Zabbix批量监控来实现。自动发现和自动注册。 1.2zabbix-server (一)1、创建自动发现规则 在“配置”->“自动发现”->“…...
2023关键词:挑战
未失踪人口回归… 好久不见,不经意间拖更2个多月。今天周末,外面淅淅沥沥下着小雨,这种窝在床上的时刻最适合写点东西了。 但是建议大家在办公或者写博客的时候尽量还是端正坐姿,我就是因为喜欢这样靠在床背上,长时间…...
Wifi wpa_supplicant 到驱动的联系
同学,别退出呀,我可是全网最牛逼的 WIFI/BT/GPS/NFC分析博主,我写了上百篇文章,请点击下面了解本专栏,进入本博主主页看看再走呗,一定不会让你后悔的,记得一定要去看主页置顶文章哦。 从framework到wpa_supplicant的适配层,其中framework部分需要了注意的是wifiservic…...
【状态估计】基于二进制粒子群优化 (BPSO) 求解最佳 PMU优化配置研究【IEEE30、39、57、118节点】(Matlab代码实现)
💥💥💞💞欢迎来到本博客❤️❤️💥💥 🏆博主优势:🌞🌞🌞博客内容尽量做到思维缜密,逻辑清晰,为了方便读者。 ⛳️座右铭&a…...
python 将 .pdf 文件转为 .md
环境准备 pip install aspose-words 代码 doc aw.Document(r"pdf 文件路径\xxx.pdf") doc.save("Output.md") 来源:https://products.aspose.com/words/zh/python-net/conversion/...
【C语言】操作符详解
每天一篇博客,卷死各位。 文章目录前言1. 算术操作符2. 移位进制位的表示移位操作符1. 》--左移操作符2. 《--右移操作符3.位操作符4.赋值操作符5.单目操作符6.关系操作符7. 逻辑操作符8.条件操作符9.逗号操作符总结前言 在c语言学习中操作符尤为重要,而…...
微信小程序 学生选课系统--nodejs+vue
系统分为学生和管理员,教师三个角色 学生小程序端的主要功能有: 1.用户注册和登陆系统 2.查看选课介绍信息 3.查看查看课程分类 4.查看课程详情,在线选课,提交选课信息 5.在线搜索课程信息 6.用户个人中心修改个人资料 7.用户查看…...
leaflet 加载geojson文件并显示图形(示例代码051)
第051个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中加载geojson文件,将图形显示在地图上。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果; 注意如果OpenStreetMap无法加载,请加载其他来练习 文章目录 示例效果配置方式示例源代码(…...
【Kafka】ZK和Kafka集群的安装和配置
一、集群环境说明1. 虚拟机:192.168.223.101/103/1052. 系统版本:CentOS 7.93. JDK版本:11.0.18.0.14. Zookeeper版本:3.7.15. Kafka版本:2.13-2.8.2备注:无论是ZK,还是Kafka,都需要…...
并发编程出现的问题以及解决方式
解决并发编程出现的问题基于java内存模式的设计出现的问题基于java内存模式的设计,多线程操作一些共享的数据时,出现以下三个问题:1.不可见性问题:多个线程同时在各自的工作内存对共享数据进行操作,彼此之间不可见。操…...
[ linux ] linux 命令英文全称及解释
🍬 博主介绍 👨🎓 博主介绍:大家好,我是 _PowerShell ,很高兴认识大家~✨主攻领域:【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】🎉点赞➕评论➕收藏 养成习…...
C++11新特性
文章目录说在前面花括号{}初始化new的列表初始化STL相关容器的列表初始化相关语法格式容器列表初始化的底层原理forward_list和array与类型相关的新特性decltype左值引用和右值引用什么是左值,什么是右值左值和右值的本质区别右值引用如何理解右值引用std::move移动…...
【宝塔部署SpringBoot前后端不分离项目】含域名访问部署、数据库、反向代理、Nginx等配置
一定要弄懂项目部署的方方面面。当服务器上部署的项目过多时,端口号什么时候该放行、什么时候才会发生冲突?多个项目使用redis怎么防止覆盖?Nginx的配置会不会产生站点冲突?二级域名如何合理配置?空闲的时候要自己用服…...
从0到1一步一步玩转openEuler--11 openEuler基础配置-设置磁盘调度算法
11 openEuler基础配置-设置磁盘调度算法 文章目录11 openEuler基础配置-设置磁盘调度算法11.1 设置磁盘调度算法11.1.1 临时修改调度策略11.1.2 永久设置调度策略11.1 设置磁盘调度算法 本节介绍如何设置磁盘调度算法。 11.1.1 临时修改调度策略 例如将所有IO调度算法修改为…...
河道治理漂浮物识别监测系统 yolov7
河道治理漂浮物识别监测系统通过yolov7网络模型深度视觉分析技术,河道治理漂浮物识别监测算法模型实时检测着河道水面是否存在漂浮物、水浮莲以及生活垃圾等,识别到河道水面存在水藻垃圾等漂浮物,立即抓拍存档预警。You Only Look Once说的是…...
微信小程序 java ssm Springboot学生作业提交管理系统
系统具有良好的集成性,提供标准接口,以实现与其他相关系统的功能和数据集成。开放性好,便于系统的升级维护、以及与各种信息系统进行集成。功能定位充分考虑平台服务对象的需求。 一个微信小程序由.js、.json、.wxml、.wxss四种文件构成&…...
咖啡网页设计毕业论文/360优化大师下载安装
将近半个月的时间,终于完成了牛腩新闻发布系统并实现了发布。可以说是从C/S到B/S的一个转变吧。只能说是一个B/S入门小系统。 初次接触B/S还是很兴奋的,对我来说,以前不知道网页是如何做出来的,这次可以自己制作一个网页还是很有成…...
网站架构策划/今日最新抗疫数据
Eclipse有多种安装插件的方式,包括在线安装、离线覆盖式安装以及离线安装插件包等。其中,在线安装常常因为网络原因需要耗费太多时间,且最终不一定安装成功。此文以安装的Java反编译插件演示。这个插件我装了好长时间才装上,所以分…...
网站建设都用什么软件/厦门网站制作全程服务
美国存在主义心理学之父罗洛梅 克尔凯郭尔(Soren Kierkegaard)开山鼻祖海德格尔(Martin Heidegger) 大师戈德斯坦(Kurt Goldstein) 提出了关于自我实现、焦虑和恐惧的观点罗洛梅书籍 《咨询的艺术ÿ…...
淘客没有网站难做/做一个公司网站要多少钱
MySQL 默认有个root用户,但是这个用户权限太大,一般只在管理数据库时候才用。如果在项目中要连接 MySQL 数据库,则建议新建一个权限较小的用户来连接。在 MySQL 命令行模式下输入如下命令可以为 MySQL 创建一个新用户:新用户创建完…...
怎么套用网站模板/长沙快速排名优化
前言 自从 Activiti 和 JBPM4 分家以后,Activiti 目前已经发展到了版本7,本着稳定性原则我们最终选择了6,之前还有一个版本5。 问题 在开发使用的过程中发现 Activiti 自带的 Web 端作图工具居然没有图片导出功能,这显然是不能满足…...
公司做网站计入那个科目/百度指数查询官网入口登录
sed s/\(.\)\{4\}$// list.txt > aa.txt...