ssm 与 shiro 下的权限管理
环境:
jdk : 1.8
spring : 5.2.10
mybatis : 3.5.4
shiro : 1.6.0
关于权限管理:
关于权限管理,个人理解为分为两种,即数据级别和访问级别。实现方式一般是权限框架、加密等技术。
1. 数据级别: 即用户是否可以请求某个接口的数据(如 /admin/user/list)。这个可以用非对称加密来解决,即服务器生成公私钥对,将私钥保留,公钥发放给接口调用者,请求时携带并在服务器端验证是否合法。
2. 访问级别: 即用户是否可以看到某个页面,是否可以点击某个按钮(一般在管理系统中)。这个可以考虑使用权限框架来实现(如 spring security、shiro 等)。实现方式大致为基于 RBAC 的权限设计。用户登录后根据用户所具有的角色权限信息生成菜单书,并将菜单树、按钮级别的权限(资源)列表、token 和用户信息等返回。首先前端将登陆成功返回的数据保存在客户端(如 localstorage)中,然后可以根据菜单树渲染菜单,其次根据按钮级别的权限(资源)列表渲染按钮,token 可以用来做接口验证、自动登录等。其实在某种意义上也实现了数据权限,比如访问用户列表,用户列表菜单是根据路由访问的,页面数据是根据接口 url 加载的。
关于 shiro:
首先,shiro 是一种权限框架…(各位看官请自行百度)。
我想说的是它的工作方式。shiro 的权限管理分为两部分,即身份验证和权限校验。身份验证就是说登录这一套流程,权限校验是指当前登录用户有没有权限去访问某个资源或请求某个接口(或者说某个资源或接口是否需要登录才可以访问)。
设计目的:
关于权限管理有很多种流程、逻辑、设计等,我想要的是在前后端分离的情况下(好像也适合不分离的情况)、对 web 应用做到数据权限 自动登录 token 验证、对管理系统做到数据权限 访问权限(按钮级别)自动登录 token 验证的一种设计。
开始搞:
1. 首先冲一杯热流奶,然后打开你最钟爱的编辑器,用你最习惯的方式创建一个 ssm 项目,在 pom 文件中加入你最喜欢的依赖。
2. 建库、建表、建实体类,设计模式是基于 RBAC 的数据库设计,代码如下:
// 基类
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class BasePo implements Serializable {
private static final long serialVersionUID = -2652207786667400142L;
private Integer id; // id
private Integer deleteFlag; // 删除状态: 0: 未删除; 1: 已删除
@JsonSerialize(using = TimeSerialize.class) // jackson 的一种自定义序列化方式
private Timestamp createTime; // 创建时间
@JsonSerialize(using = TimeSerialize.class)
private Timestamp updateTime; // 修改时间
private Integer currentPage = 1; // 当前页(默认 第一页)
private Integer pageSize = 10; // 页面大小(默认 10)
}
// 权限(资源)类 table name sys_perm
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class SYSPermPo extends BasePo implements Serializable {
private static final long serialVersionUID = 8271814788608471811L;
private Integer parentId; //父菜单id
private String permName; //权限名称(菜单名称)
private String permUrl; //权限url(即接口路径 如 /admin/user/list)
private String routerComponent; //路由组件(这里特指在 vue 项目中的页面组件位置(如 @/views/admin/user/index))
private String routerPath; //路由 path(这里特指 vue 项目中的页面访问路径(即 router))
private String routerIcon; //路由 icon(即菜单图标 icon 地址)
private Integer routerHidden; //路由 hidden 值:0:显示;1:隐藏
private Integer permType; //权限类型:0: 全部; 1:菜单;2:子菜单;3:操作
private Integer permSort; //权限排序
}
// 角色类 table name sys_role
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class SYSRolePo extends BasePo implements Serializable {
private static final long serialVersionUID = 2956381569963543019L;
private String roleNamey; //英文名
private String roleNamez; //中文名
}
// 用户类 table name sys_user
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class SYSUserPo extends BasePo implements Serializable {
private static final long serialVersionUID = 7290300678694098264L;
private String username; // 用户名
private String password; // 密码
private Integer userType; // 用户类型: 0: 全部; 1: 管理员; 2: 其它
}
// 角色-权限(资源)类 table name sys_role_perm
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class SYSRolePermPo extends BasePo implements Serializable {
private static final long serialVersionUID = -5728135742571817968L;
private Integer roleId; //角色id
private Integer permId; //权限id
}
// 用户-角色 类 table name sys_user_role
@Data
@ToString
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class SYSUserRolePo extends BasePo implements Serializable {
private static final long serialVersionUID = -6450223660671697806L;
private Integer userId; //用户id
private Integer roleId; //角色id
}
3. web.xml 配置,主要配置 spring、springmvc、shiro 等。代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<display-name>Archetype Created Web Application</display-name>
<!-- spring 配置 -->
<!-- springmvc 的初始化依赖于 spring 容器的初始化(IOC AOP) -->
<!-- spring 配置文件 通过 contextConfigLocation 参数来指定 默认以 /WEB-INF/applicationContext.xml 为配置文件 -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/applicationContext.xml</param-value>
</context-param>
<!-- spring 核心监听器 -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- shiro 配置 -->
<filter>
<filter-name>shiroFilter</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>shiroFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- springmvc 配置 -->
<!-- springmvc 配置文件默认以 /WEB-INF/<servlet-name>-servlet.xml 为配置文件 -->
<!-- 执行顺序: url -> servlet-name -> servlet-class -->
<servlet>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
4. springmvc-servlet.xml 配置(springmvc),主要配置 spring 注解、自动装配、试图解析等。代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc https://www.springframework.org/schema/mvc/spring-mvc.xsd">
<!-- 启用 spring 注解 -->
<context:annotation-config />
<!-- 配置注解驱动 -->
<mvc:annotation-driven />
<!-- 自动扫描装配 -->
<context:component-scan base-package="org.xgllhz.base.controller"></context:component-scan>
<!-- 静态资源特殊处理(如果是静态资源则交给 web 服务器默认的 servlet 处理,否则交给 DispatcherServlet 处理) -->
<mvc:default-servlet-handler />
<!-- 视图解析器 -->
<bean id="defaultViewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/>
<property name="prefix" value="/WEB-INF/views"/>
<property name="suffix" value=".jsp"/>
<property name="exposeContextBeansAsAttributes" value="true"/>
</bean>
</beans>
5. applicationContext.xml 配置(spring等),主要配置数据源、mybatis、事务、shiro 等。代码如下:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:util="http://www.springframework.org/schema/util"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/util https://www.springframework.org/schema/util/spring-util.xsd">
<!-- 自动扫描包 -->
<context:component-scan base-package="org.xgllhz.base" />
<!-- 配置数据源(spring 推荐使用这种方式,PropertyPlaceholderConfigurer 已被弃用) -->
<context:property-placeholder location="classpath:config/mysql.properties" />
<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${driver}" /> <!-- 驱动类 -->
<property name="url" value="${url}" /> <!-- url -->
<property name="username" value="${user}" /> <!-- 用户名 -->
<property name="password" value="${password}" /> <!-- 密码 -->
<property name="initialSize" value="${initialSize}" /> <!-- 初始化连接数 -->
<property name="maxTotal" value="${maxTotal}" /> <!-- 最大连接数 -->
<property name="maxIdle" value="${maxIdle}" /> <!-- 最大空闲数 -->
<property name="minIdle" value="${minIdle}" /> <!-- 最小空闲数 -->
<property name="maxWaitMillis" value="${maxWaitMillis}" /> <!-- 最长等待时间 -->
</bean>
<!-- spring 与 mybatis 配置 -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
<property name="dataSource" ref="dataSource" /> <!-- 数据源 -->
<property name="configLocation" value="classpath:config/spring-mybatis.xml" /> <!-- mybatis 配置文件 -->
<property name="mapperLocations" value="classpath:mapper/*.xml" /> <!-- xml -->
</bean>
<!-- 扫描 mapper -->
<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
<property name="basePackage" value="org.xgllhz.base.mapper" />
<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
</bean>
<!-- 事务管理 -->
<bean id="transactionManager"
class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<!-- shiro 配置 -->
<bean id="shiroFilter" class="org.xgllhz.base.config.ShiroFactoryBean">
<property name="securityManager" ref="securityManager" /> <!-- 权限管理器 -->
<!-- 权限不足时跳转(非必须属性),shiro 默认跳转是 login.jsp,也可以自定义页面,这里直接返回 json(权限不足状态码)
适用于前后端分离-->
<property name="loginUrl" value="/admin/user/authority_error" />
<property name="successUrl" value="/" /> <!-- 登陆成功后跳转的地址 -->
<property name="unauthorizedUrl" value="/admin/user/authorizingFail" /> <!-- 这个好像没必要? -->
<!-- 过滤链,默认在这里配置,也可从数据库动态加载,shiro 的过滤链有四类,即 authc user logout anon
authc: 需要认证才可以访问(这里由固定配置和数据库动态加载组成,详见 ShiroFactoryBean)
user: 需要认证或记住我才可以访问
logout: 登出 url
anon: 不需要认证即可访问 -->
<property name="filterChainDefinitions">
<value>
/superadmin/** = authc
</value>
</property>
</bean>
<!-- shiro 权限管理器 -->
<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<property name="realm" ref="shiroRealm" />
</bean>
<!-- shiro 自定义 realm -->
<bean id="shiroRealm" class="org.xgllhz.base.config.ShiroRealm" />
</beans>
6. ShiroFactoryBean,继承 shiro 中的 ShiroFilterFactoryBeaan,重写了里面的 setFilterChainDefinitions 方法。首先这个类是在项目启动(容器初始化时)时加载配置文件中关于 shiro 的配置,这个方法是将加载到的配置文件中的过滤链(这里可以理解为需要授权才能访问的资源)设置到 shiro 中。我们重写这个方法的意义是让 shiro 能够从数据库中获取权限(资源)数据生成过滤链,以便进行动态管理。代码如下:
public class ShiroFactoryBean extends ShiroFilterFactoryBean {
private final SYSPermMapper permMapper;
public ShiroFactoryBean(SYSPermMapper permMapper) {
this.permMapper = permMapper;
}
// 这个变量为配置文件中 shiro 配置的过滤链
public static String filterChainDefinitions = "";
// perms 为 shiro 配置文件中默认变量,意为访问某个资源需要权限 perms['/admin/**']
public static final String PERMISSION_STRING = "perms[\"{0}\"]";
/**
* 设置过滤链
* 项目启动时 会从数据库加载过滤链(权限(资源)表),并将其添加到 shiro 配置文件中的过滤链中
* @param chainDefinitions shiro 配置文件中配置的过滤链
*/
@Override
public void setFilterChainDefinitions(String chainDefinitions) {
ShiroFactoryBean.filterChainDefinitions = chainDefinitions; // shiro 配置文件中的过滤连
Map<String, String> filterChains = new HashMap<>(); // 承载过滤链的变量
// 从数据库中获取权限(资源)url
List<SYSPermPo> list = permMapper.getList(new SYSPermPo());
if (list != null && list.size() != 0) {
list.stream().peek(a -> {
if (StringUtils.isNoneBlank(a.getPermUrl())) {
filterChains.put(a.getPermUrl(), MessageFormat.format(PERMISSION_STRING, a.getPermUrl()));
}
}).collect(Collectors.toList());
}
// 记载配置文件中的过滤连
Ini ini = new Ini();
ini.load(filterChainDefinitions);
Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
if (CollectionUtils.isEmpty(section)) {
section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
}
// 数据库中的动态过滤连
section.putAll(filterChains);
setFilterChainDefinitionMap(section);
}
}
7. ShiroFilterChainRefresh,这个类的作用是刷新过滤链。为什么要刷新呢,因为过滤链是项目启动时由获取的数据库中的数据生成的,当我们修改了数据库中的相关数据时不可能重启项目,所以需要动态加载(或者说动态改变、刷新)。刷新方式是在新增、删除或修改了权限(资源)表(sys_perm)中的数据后调用 ShiroFilterChainRefresh 中的 reloadFilterChain 方法。代码如下:
// 刷新方式 注意在新增、删除和修改了之后都得刷新
@Service
public class SYSPermServiceImpl implements SYSPermService {
private final SYSPermMapper permMapper;
private final ShiroFilterChainRefresh chainRefresh;
public SYSPermServiceImpl(SYSPermMapper permMapper, ShiroFilterChainRefresh chainRefresh) {
this.permMapper = permMapper;
this.chainRefresh = chainRefresh;
}
@Override
@Transactional()
public APIResponse<Map<String, Object>> insertPerm(SYSPermPo permPo) {
if (permPo.getParentId() != null && permPo.getParentId() != 0) {
permPo.setId(permPo.getParentId());
SYSPermPo permPo1 = permMapper.getPerm(permPo);
if (permPo1 == null) {
throw new GlobalException(ConstConfig.RE_INSERT_ERROR_CODE, "父节点数据不存在!");
}
}
int res = permMapper.insertPerm(permPo);
if (res <= 0) {
throw new GlobalException(ConstConfig.RE_INSERT_ERROR_CODE, "新增权限(资源)失败!");
}
chainRefresh.reloadFilterChain(); // 刷新过滤链
return new APIResponse<>();
}
}
// ShiroFilterChainRefresh
@Service
public class ShiroFilterChainRefresh {
private final ShiroFactoryBean shiroFactoryBean;
public ShiroFilterChainRefresh(ShiroFactoryBean shiroFactoryBean) {
this.shiroFactoryBean = shiroFactoryBean;
}
/**
* 重新加载过滤链(线程安全)
*/
public void reloadFilterChain() {
synchronized (shiroFactoryBean) {
AbstractShiroFilter filter;
try {
// 获取 shiro 拦截器实例
filter = (AbstractShiroFilter) shiroFactoryBean.getObject();
// 获取路径匹配过滤链解析器实例
PathMatchingFilterChainResolver resolver = (PathMatchingFilterChainResolver) filter.getFilterChainResolver();
// 获取默认过滤链管理器
DefaultFilterChainManager manager = (DefaultFilterChainManager) resolver.getFilterChainManager();
// 清除过滤链
manager.getFilterChains().clear();
shiroFactoryBean.getFilterChainDefinitionMap().clear();
// 设置过滤链目标
shiroFactoryBean.setFilterChainDefinitions(ShiroFactoryBean.filterChainDefinitions);
// 重新生成过滤链
Map<String, String> filterChains = shiroFactoryBean.getFilterChainDefinitionMap();
if (!CollectionUtils.isEmpty(filterChains)) {
for (Map.Entry<String, String> chain : filterChains.entrySet()) {
manager.createChain(chain.getKey(), chain.getValue().replace(" ", ""));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
8. ShiroRealm,这个类的作用是获取权限校验和身份验证所需要的信息,以便进行权限校验和身份验证。该类继承了 shiro 中的 AuthorizingRealm 类,重写了其中的 doGetAuthorizationInfo 和 doGetAuthenticationInfo 方法。关于这两个方法的作用、调用时间和特点等都在代码中的注释里。代码如下:
public class ShiroRealm extends AuthorizingRealm {
private final SYSUserMapper userMapper;
public ShiroRealm(SYSUserMapper userMapper) {
this.userMapper = userMapper;
}
/**
* 权限校验
* 获取权限校验需要的信息(当前登录用户所具有的权限信息:权限、角色)
* 调用时间: 1、默认情况下是每次请求资源(url)时都会调用;
* 2、但可以将用户权限(资源)信息存放到缓存中,存放方式是在 subject.login()
* 之后调用 subject.isPermitted("test")方法,在这个方法内部会调用下面的
* 方法来获取用户权限(资源)信息,同时需要在 shiro 配置文件中配置相应的缓存管理器;
* 3、放入缓存之后只在每次登录时调用;
* 检验方式: shiro 的权限校验方式有两种,即基于权限(资源)、基于角色
* 此项目采用基于权限(资源)的方式
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 获取登录成功的用户名
// 这里会从 session 中获取(登录执行 subject.login 方法时会将用户信息放入 session)
String username = (String) principalCollection.fromRealm(getName()).iterator().next();
// 根据用户名获取该用户所具有的权限(资源)或角色
// 注: 当校验方式为基于权限(资源)时,这里可不用获取角色信息
List<SYSPermPo> permList = userMapper.getPermListByUsername(username);
//List<SYSRolePo> roleList = userMapper.getRoleListByUsername(username);
List<String> perms = null;
//List<String> roles = null;
if (permList != null && permList.size() != 0) {
perms = permList.stream().map(SYSPermPo::getPermUrl).collect(Collectors.toList());
}
/*if (roleList != null && roleList.size() != 0) {
roles = roleList.stream().map(SYSRolePo::getRoleNamey).collect(Collectors.toList());
}*/
//
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.addStringPermissions(perms);
//info.addRoles(roles);
return info;
}
/**
* 身份验证
* 获取身份验证需要的信息(数据库中的用户数据)
* 调用时间: 登录时调用,即请求 /admin/user/login时(service 中 的 subject.login())
* @param authenticationToken
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 用户登录提交的信息
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
String username = token.getUsername();
if (StringUtils.isBlank(username)) {
throw new GlobalException(ConstConfig.RE_NAME_OR_PASSWORD_ERROR_CODE, ConstConfig.RE_NAME_OR_PASSWORD_ERROR_MSG);
}
// 根据用户名查询到的信息
SYSUserPo userPo = userMapper.getUserByUsername(username);
if (userPo == null) {
throw new GlobalException(ConstConfig.RE_USERNAME_NOT_EXIST_ERROR_CODE, ConstConfig.RE_USERNAME_NOT_EXIST_ERROR_MSG);
}
return new SimpleAuthenticationInfo(userPo.getUsername(), userPo.getPassword(), getName());
}
}
9. 登录接口,即 /admin/user/login 接口。代码如下:
// user controller
@RestController
@RequestMapping("/admin/user")
public class SYSUserController {
private final SYSUserService userService;
public SYSUserController(SYSUserService userService) {
this.userService = userService;
}
// 登录接口
@RequestMapping("/login")
public APIResponse<Map<String, Object>> login(@RequestBody SYSUserPo userPo) {
if (StringUtils.isBlank(userPo.getUsername()) || StringUtils.isBlank(userPo.getPassword())) {
return new APIResponse<>(ConstConfig.RE_NAME_OR_PASSWORD_ERROR_CODE, ConstConfig.RE_NAME_OR_PASSWORD_ERROR_MSG);
}
return userService.login(userPo);
}
// 授权失败接口 前端可根据此接口做出相应响应
@RequestMapping("/authority_error")
public APIResponse<Map<String, Object>> authorityError() {
return new APIResponse<>(ConstConfig.RE_AUTHORITY_ERROR_CODE, ConstConfig.RE_AUTHORITY_ERROR_MSG);
}
}
// user service impl
@Service
public class SYSUserServiceImpl implements SYSUserService {
private final SYSUserMapper userMapper;
public SYSUserServiceImpl(SYSUserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public APIResponse<Map<String, Object>> login(SYSUserPo userPo) {
try {
// 调用 shiro 中的登录方法
UsernamePasswordToken token = new UsernamePasswordToken(userPo.getUsername(), userPo.getPassword());
Subject subject = SecurityUtils.getSubject();
subject.login(token); // 调用 shiro 的登录方法
// subject.login 方法执行时会将用户信息放入 session,默认有效时间为 30m
subject.getSession().setTimeout(-1); // 设置 session 永不过期(在登录时会刷新)
// TODO 将用户、token、菜单树等信息返回(懒,所以目前只返回了用户信息)
SYSUserPo userPo1 = userMapper.getUserByUsername(userPo.getUsername());
if (userPo1 == null) {
throw new GlobalException(ConstConfig.RE_USERNAME_NOT_EXIST_ERROR_CODE, ConstConfig.RE_USERNAME_NOT_EXIST_ERROR_MSG);
}
Map<String, Object> map = new HashMap<>();
map.put(ConstConfig.DATA_INFO, userPo1);
return new APIResponse<>(map);
} catch (AuthenticationException e) {
return new APIResponse<>(ConstConfig.RE_NAME_OR_PASSWORD_ERROR_CODE, ConstConfig.RE_NAME_OR_PASSWORD_ERROR_MSG);
}
/** 注:关于 token 以及 session 设置永不过期,是因为我不想用 cookie,
要想做到用户长时间保持登录(即十天内、五天内登录),在执行完 subject.login
方法后会将用户信息保存到 session 中,有效时间默认是 30m,时间一过,
请求某个需要授权访问的资源时就会没有权限(上面提到处理每个请求都会调用
doGetAuthorizationInfo 方法,这个方法里是通过保存在 session 中的用户信息
去获取该用户的权限信息的,session 失效即为没有权限),所以我们将 session
设置为永不过期(登录时会自动刷新),然后创建有效期为一定时间(10天、一个月)
的 token,前端将 token 存储在 localstorage 中,请求时携带,服务器处理请求时
验证 token(可自定义注解作用在 controller 层),当 token 未过期时通过,
过期时返回 token 过期重新登录返回码。*/
}
}
想想还有没有漏掉啥的,嗯…好像没有。
Pure and desirous and wild!
嗯…确实没有漏掉啥!