第八章 拦截器机制——《跟我学Shiro》

8.1 拦截器介绍

Shiro使用了与Servlet一样的Filter接口进行扩展;所以如果对Filter不熟悉可以参考《Servlet3.1规范》http://www.iteye.com/blogs/subjects/Servlet-3-1了解Filter的工作原理。首先下图是Shiro拦截器的基础类图:

1、NameableFilter

NameableFilter给Filter起个名字,如果没有设置默认就是FilterName;还记得之前的如authc吗?当我们组装拦截器链时会根据这个名字找到相应的拦截器实例;

2、OncePerRequestFilter

OncePerRequestFilter用于防止多次执行Filter的;也就是说一次请求只会走一次拦截器链;另外提供enabled属性,表示是否开启该拦截器实例,默认enabled=true表示开启,如果不想让某个拦截器工作,可以设置为false即可。

3、ShiroFilter

ShiroFilter是整个Shiro的入口点,用于拦截需要安全控制的请求进行处理,这个之前已经用过了。

4、AdviceFilter

AdviceFilter提供了AOP风格的支持,类似于SpringMVC中的Interceptor:

  1. boolean preHandle(ServletRequest request, ServletResponse response) throws Exception
  2. void postHandle(ServletRequest request, ServletResponse response) throws Exception
  3. void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception;

preHandler:类似于AOP中的前置增强;在拦截器链执行之前执行;如果返回true则继续拦截器链;否则中断后续的拦截器链的执行直接返回;进行预处理(如基于表单的身份验证、授权)

postHandle:类似于AOP中的后置返回增强;在拦截器链执行完成后执行;进行后处理(如记录执行时间之类的);

afterCompletion:类似于AOP中的后置最终增强;即不管有没有异常都会执行;可以进行清理资源(如接触Subject与线程的绑定之类的);

5、PathMatchingFilter

PathMatchingFilter提供了基于Ant风格的请求路径匹配功能及拦截器参数解析的功能,如“roles[admin,user]”自动根据“,”分割解析到一个路径参数配置并绑定到相应的路径:

  1. boolean pathsMatch(String path, ServletRequest request)
  2. boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception

pathsMatch:该方法用于path与请求路径进行匹配的方法;如果匹配返回true;

onPreHandle:在preHandle中,当pathsMatch匹配一个路径后,会调用opPreHandler方法并将路径绑定参数配置传给mappedValue;然后可以在这个方法中进行一些验证(如角色授权),如果验证失败可以返回false中断流程;默认返回true;也就是说子类可以只实现onPreHandle即可,无须实现preHandle。如果没有path与请求路径匹配,默认是通过的(即preHandle返回true)。

6、AccessControlFilter

AccessControlFilter提供了访问控制的基础功能;比如是否允许访问/当访问拒绝时如何处理等:

  1. abstract boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
  2. boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception;
  3. abstract boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception;

isAccessAllowed:表示是否允许访问;mappedValue就是[urls]配置中拦截器参数部分,如果允许访问返回true,否则false;

onAccessDenied:表示当访问拒绝时是否已经处理了;如果返回true表示需要继续处理;如果返回false表示该拦截器实例已经处理了,将直接返回即可。

onPreHandle会自动调用这两个方法决定是否继续处理:

  1. boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
  2. return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
  3. }

另外AccessControlFilter还提供了如下方法用于处理如登录成功后/重定向到上一个请求:

  1. void setLoginUrl(String loginUrl) //身份验证时使用,默认/login.jsp
  2. String getLoginUrl()
  3. Subject getSubject(ServletRequest request, ServletResponse response) //获取Subject实例
  4. boolean isLoginRequest(ServletRequest request, ServletResponse response)//当前请求是否是登录请求
  5. void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException //将当前请求保存起来并重定向到登录页面
  6. void saveRequest(ServletRequest request) //将请求保存起来,如登录成功后再重定向回该请求
  7. void redirectToLogin(ServletRequest request, ServletResponse response) //重定向到登录页面

比如基于表单的身份验证就需要使用这些功能。

到此基本的拦截器就完事了,如果我们想进行访问访问的控制就可以继承AccessControlFilter;如果我们要添加一些通用数据我们可以直接继承PathMatchingFilter。

8.2 拦截器链

Shiro对Servlet容器的FilterChain进行了代理,即ShiroFilter在继续Servlet容器的Filter链的执行之前,通过ProxiedFilterChain对Servlet容器的FilterChain进行了代理;即先走Shiro自己的Filter体系,然后才会委托给Servlet容器的FilterChain进行Servlet容器级别的Filter链执行;Shiro的ProxiedFilterChain执行流程:1、先执行Shiro自己的Filter链;2、再执行Servlet容器的Filter链(即原始的Filter)。

而ProxiedFilterChain是通过FilterChainResolver根据配置文件中[urls]部分是否与请求的URL是否匹配解析得到的。

  1. FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain);

即传入原始的chain得到一个代理的chain。

Shiro内部提供了一个路径匹配的FilterChainResolver实现:PathMatchingFilterChainResolver,其根据[urls]中配置的url模式(默认Ant风格)=拦截器链和请求的url是否匹配来解析得到配置的拦截器链的;而PathMatchingFilterChainResolver内部通过FilterChainManager维护着拦截器链,比如DefaultFilterChainManager实现维护着url模式与拦截器链的关系。因此我们可以通过FilterChainManager进行动态动态增加url模式与拦截器链的关系。

DefaultFilterChainManager会默认添加org.apache.shiro.web.filter.mgt.DefaultFilter中声明的拦截器:

  1. public enum DefaultFilter {
  2. anon(AnonymousFilter.class),
  3. authc(FormAuthenticationFilter.class),
  4. authcBasic(BasicHttpAuthenticationFilter.class),
  5. logout(LogoutFilter.class),
  6. noSessionCreation(NoSessionCreationFilter.class),
  7. perms(PermissionsAuthorizationFilter.class),
  8. port(PortFilter.class),
  9. rest(HttpMethodPermissionFilter.class),
  10. roles(RolesAuthorizationFilter.class),
  11. ssl(SslFilter.class),
  12. user(UserFilter.class);
  13. }

下一节会介绍这些拦截器的作用。

如果要注册自定义拦截器,IniSecurityManagerFactory/WebIniSecurityManagerFactory在启动时会自动扫描ini配置文件中的[filters]/[main]部分并注册这些拦截器到DefaultFilterChainManager;且创建相应的url模式与其拦截器关系链。如果使用Spring后续章节会介绍如果注册自定义拦截器。

如果想自定义FilterChainResolver,可以通过实现WebEnvironment接口完成:

  1. public class MyIniWebEnvironment extends IniWebEnvironment {
  2. @Override
  3. protected FilterChainResolver createFilterChainResolver() {
  4. //在此处扩展自己的FilterChainResolver
  5. return super.createFilterChainResolver();
  6. }
  7. }

FilterChain之间的关系。如果想动态实现url-拦截器的注册,就可以通过实现此处的FilterChainResolver来完成,比如:

  1. //1、创建FilterChainResolver
  2. PathMatchingFilterChainResolver filterChainResolver =
  3. new PathMatchingFilterChainResolver();
  4. //2、创建FilterChainManager
  5. DefaultFilterChainManager filterChainManager = new DefaultFilterChainManager();
  6. //3、注册Filter
  7. for(DefaultFilter filter : DefaultFilter.values()) {
  8. filterChainManager.addFilter(
  9. filter.name(), (Filter) ClassUtils.newInstance(filter.getFilterClass()));
  10. }
  11. //4、注册URL-Filter的映射关系
  12. filterChainManager.addToChain("/login.jsp", "authc");
  13. filterChainManager.addToChain("/unauthorized.jsp", "anon");
  14. filterChainManager.addToChain("/**", "authc");
  15. filterChainManager.addToChain("/**", "roles", "admin");
  16. //5、设置Filter的属性
  17. FormAuthenticationFilter authcFilter =
  18. (FormAuthenticationFilter)filterChainManager.getFilter("authc");
  19. authcFilter.setLoginUrl("/login.jsp");
  20. RolesAuthorizationFilter rolesFilter =
  21. (RolesAuthorizationFilter)filterChainManager.getFilter("roles");
  22. rolesFilter.setUnauthorizedUrl("/unauthorized.jsp");
  23. filterChainResolver.setFilterChainManager(filterChainManager);
  24. return filterChainResolver;

此处自己去实现注册filter,及url模式与filter之间的映射关系。可以通过定制FilterChainResolver或FilterChainManager来完成诸如动态URL匹配的实现。

然后再web.xml中进行如下配置Environment:

  1. <context-param>
  2. <param-name>shiroEnvironmentClass</param-name> <param-value>com.github.zhangkaitao.shiro.chapter8.web.env.MyIniWebEnvironment</param-value>
  3. </context-param>

8.3 自定义拦截器

通过自定义自己的拦截器可以扩展一些功能,诸如动态url-角色/权限访问控制的实现、根据Subject身份信息获取用户信息绑定到Request(即设置通用数据)、验证码验证、在线用户信息的保存等等,因为其本质就是一个Filter;所以Filter能做的它就能做。

对于Filter的介绍请参考《Servlet规范》中的Filter部分:

http://www.iteye.com/blogs/subjects/Servlet-3-1

1、扩展OncePerRequestFilter

OncePerRequestFilter保证一次请求只调用一次doFilterInternal,即如内部的forward不会再多执行一次doFilterInternal:

  1. public class MyOncePerRequestFilter extends OncePerRequestFilter {
  2. @Override
  3. protected void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
  4. System.out.println("=========once per request filter");
  5. chain.doFilter(request, response);
  6. }
  7. }

然后再shiro.ini配置文件中:

  1. [main]
  2. myFilter1=com.github.zhangkaitao.shiro.chapter8.web.filter.MyOncePerRequestFilter
  3. #[filters]
  4. #myFilter1=com.github.zhangkaitao.shiro.chapter8.web.filter.MyOncePerRequestFilter
  5. [urls]
  6. /**=myFilter1

Filter可以在[main]或[filters]部分注册,然后在[urls]部分配置url与filter的映射关系即可。

2、扩展AdviceFilter

AdviceFilter提供了AOP的功能,其实现和SpringMVC中的Interceptor思想一样:具体可参考我的SpringMVC教程中的处理器拦截器部分:

http://www.iteye.com/blogs/subjects/kaitao-springmvc

  1. public class MyAdviceFilter extends AdviceFilter {
  2. @Override
  3. protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
  4. System.out.println("====预处理/前置处理");
  5. return true;//返回false将中断后续拦截器链的执行
  6. }
  7. @Override
  8. protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {
  9. System.out.println("====后处理/后置返回处理");
  10. }
  11. @Override
  12. public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
  13. System.out.println("====完成处理/后置最终处理");
  14. }
  15. }

preHandle:进行请求的预处理,然后根据返回值决定是否继续处理(true:继续过滤器链);可以通过它实现权限控制;

postHandle:执行完拦截器链之后正常返回后执行;

afterCompletion:不管最后有没有异常,afterCompletion都会执行,完成如清理资源功能。

然后在shiro.ini中进行如下配置:

  1. [filters]
  2. myFilter1=com.github.zhangkaitao.shiro.chapter8.web.filter.MyOncePerRequestFilter
  3. myFilter2=com.github.zhangkaitao.shiro.chapter8.web.filter.MyAdviceFilter
  4. [urls]
  5. /**=myFilter1,myFilter2

该过滤器的具体使用可参考我的SpringMVC教程中的处理器拦截器部分。

3PathMatchingFilter

PathMatchingFilter继承了AdviceFilter,提供了url模式过滤的功能,如果需要对指定的请求进行处理,可以扩展PathMatchingFilter:

  1. public class MyPathMatchingFilter extends PathMatchingFilter {
  2. @Override
  3. protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
  4. System.out.println("url matches,config is " + Arrays.toString((String[])mappedValue));
  5. return true;
  6. }
  7. }

preHandle:会进行url模式与请求url进行匹配,如果匹配会调用onPreHandle;如果没有配置url模式/没有url模式匹配,默认直接返回true;

onPreHandle:如果url模式与请求url匹配,那么会执行onPreHandle,并把该拦截器配置的参数传入。默认什么不处理直接返回true。

然后在shiro.ini中进行如下配置:

  1. [filters]
  2. myFilter3=com.github.zhangkaitao.shiro.chapter8.web.filter.MyPathMatchingFilter
  3. [urls]
  4. /**= myFilter3[config]

/**就是注册给PathMatchingFilter的url模式,config就是拦截器的配置参数,多个之间逗号分隔,onPreHandle使用mappedValue接收参数值。

4、扩展AccessControlFilter

AccessControlFilter继承了PathMatchingFilter,并扩展了了两个方法:

  1. public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
  2. return isAccessAllowed(request, response, mappedValue)
  3. || onAccessDenied(request, response, mappedValue);
  4. }

isAccessAllowed:即是否允许访问,返回true表示允许;

onAccessDenied:表示访问拒绝时是否自己处理,如果返回true表示自己不处理且继续拦截器链执行,返回false表示自己已经处理了(比如重定向到另一个页面)。

  1. public class MyAccessControlFilter extends AccessControlFilter {
  2. protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
  3. System.out.println("access allowed");
  4. return true;
  5. }
  6. protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
  7. System.out.println("访问拒绝也不自己处理,继续拦截器链的执行");
  8. return true;
  9. }
  10. }

然后在shiro.ini中进行如下配置:

  1. [filters]
  2. myFilter4=com.github.zhangkaitao.shiro.chapter8.web.filter.MyAccessControlFilter
  3. [urls]
  4. /**=myFilter4

5、基于表单登录拦截器

之前我们已经使用过Shiro内置的基于表单登录的拦截器了,此处自己做一个类似的基于表单登录的拦截器。

  1. public class FormLoginFilter extends PathMatchingFilter {
  2. private String loginUrl = "/login.jsp";
  3. private String successUrl = "/";
  4. @Override
  5. protected boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
  6. if(SecurityUtils.getSubject().isAuthenticated()) {
  7. return true;//已经登录过
  8. }
  9. HttpServletRequest req = (HttpServletRequest) request;
  10. HttpServletResponse resp = (HttpServletResponse) response;
  11. if(isLoginRequest(req)) {
  12. if("post".equalsIgnoreCase(req.getMethod())) {//form表单提交
  13. boolean loginSuccess = login(req); //登录
  14. if(loginSuccess) {
  15. return redirectToSuccessUrl(req, resp);
  16. }
  17. }
  18. return true;//继续过滤器链
  19. } else {//保存当前地址并重定向到登录界面
  20. saveRequestAndRedirectToLogin(req, resp);
  21. return false;
  22. }
  23. }
  24. private boolean redirectToSuccessUrl(HttpServletRequest req, HttpServletResponse resp) throws IOException {
  25. WebUtils.redirectToSavedRequest(req, resp, successUrl);
  26. return false;
  27. }
  28. private void saveRequestAndRedirectToLogin(HttpServletRequest req, HttpServletResponse resp) throws IOException {
  29. WebUtils.saveRequest(req);
  30. WebUtils.issueRedirect(req, resp, loginUrl);
  31. }
  32. private boolean login(HttpServletRequest req) {
  33. String username = req.getParameter("username");
  34. String password = req.getParameter("password");
  35. try {
  36. SecurityUtils.getSubject().login(new UsernamePasswordToken(username, password));
  37. } catch (Exception e) {
  38. req.setAttribute("shiroLoginFailure", e.getClass());
  39. return false;
  40. }
  41. return true;
  42. }
  43. private boolean isLoginRequest(HttpServletRequest req) {
  44. return pathsMatch(loginUrl, WebUtils.getPathWithinApplication(req));
  45. }
  46. }

onPreHandle主要流程:

1、首先判断是否已经登录过了,如果已经登录过了继续拦截器链即可;

2、如果没有登录,看看是否是登录请求,如果是get方法的登录页面请求,则继续拦截器链(到请求页面),否则如果是get方法的其他页面请求则保存当前请求并重定向到登录页面;

3、如果是post方法的登录页面表单提交请求,则收集用户名/密码登录即可,如果失败了保存错误消息到“shiroLoginFailure”并返回到登录页面;

4、如果登录成功了,且之前有保存的请求,则重定向到之前的这个请求,否则到默认的成功页面。

shiro.ini配置

  1. [filters]
  2. formLogin=com.github.zhangkaitao.shiro.chapter8.web.filter.FormLoginFilter
  3. [urls]
  4. /test.jsp=formLogin
  5. /login.jsp=formLogin

启动服务器输入http://localhost:8080/chapter8/test.jsp测试时,会自动跳转到登录页面,登录成功后又会跳回到test.jsp页面。

此处可以通过继承AuthenticatingFilter实现,其提供了很多登录相关的基础代码。另外可以参考Shiro内嵌的FormAuthenticationFilter的源码,思路是一样的。

6、任意角色授权拦截器

Shiro提供roles拦截器,其验证用户拥有所有角色,没有提供验证用户拥有任意角色的拦截器。

  1. public class AnyRolesFilter extends AccessControlFilter {
  2. private String unauthorizedUrl = "/unauthorized.jsp";
  3. private String loginUrl = "/login.jsp";
  4. protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
  5. String[] roles = (String[])mappedValue;
  6. if(roles == null) {
  7. return true;//如果没有设置角色参数,默认成功
  8. }
  9. for(String role : roles) {
  10. if(getSubject(request, response).hasRole(role)) {
  11. return true;
  12. }
  13. }
  14. return false;//跳到onAccessDenied处理
  15. }
  16. @Override
  17. protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
  18. Subject subject = getSubject(request, response);
  19. if (subject.getPrincipal() == null) {//表示没有登录,重定向到登录页面
  20. saveRequest(request);
  21. WebUtils.issueRedirect(request, response, loginUrl);
  22. } else {
  23. if (StringUtils.hasText(unauthorizedUrl)) {//如果有未授权页面跳转过去
  24. WebUtils.issueRedirect(request, response, unauthorizedUrl);
  25. } else {//否则返回401未授权状态码
  26. WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
  27. }
  28. }
  29. return false;
  30. }
  31. }

流程:

1、首先判断用户有没有任意角色,如果没有返回false,将到onAccessDenied进行处理;

2、如果用户没有角色,接着判断用户有没有登录,如果没有登录先重定向到登录;

3、如果用户没有角色且设置了未授权页面(unauthorizedUrl),那么重定向到未授权页面;否则直接返回401未授权错误码。

shiro.ini配置

  1. [filters]
  2. anyRoles=com.github.zhangkaitao.shiro.chapter8.web.filter.AnyRolesFilter
  3. [urls]
  4. /test.jsp=formLogin,anyRoles[admin,user]
  5. /login.jsp=formLogin

此处可以继承AuthorizationFilter实现,其提供了授权相关的基础代码。另外可以参考Shiro内嵌的RolesAuthorizationFilter的源码,只是实现hasAllRoles逻辑。

8.4 默认拦截器

Shiro内置了很多默认的拦截器,比如身份验证、授权等相关的。默认拦截器可以参考org.apache.shiro.web.filter.mgt.DefaultFilter中的枚举拦截器:

默认拦截器名

拦截器类

说明(括号里的表示默认值)

身份验证相关的

authc

org.apache.shiro.web.filter.authc

.FormAuthenticationFilter

基于表单的拦截器;如“/**=authc”,如果没有登录会跳到相应的登录页面登录;主要属性:usernameParam:表单提交的用户名参数名( username);  passwordParam:表单提交的密码参数名(password); rememberMeParam:表单提交的密码参数名(rememberMe);  loginUrl:登录页面地址(/login.jsp);successUrl:登录成功后的默认重定向地址; failureKeyAttribute:登录失败后错误信息存储key(shiroLoginFailure);

authcBasic

org.apache.shiro.web.filter.authc

.BasicHttpAuthenticationFilter

Basic HTTP身份验证拦截器,主要属性: applicationName:弹出登录框显示的信息(application);

logout

org.apache.shiro.web.filter.authc

.LogoutFilter

退出拦截器,主要属性:redirectUrl:退出成功后重定向的地址(/);示例“/logout=logout”

user

org.apache.shiro.web.filter.authc

.UserFilter

用户拦截器,用户已经身份验证/记住我登录的都可;示例“/**=user”

anon

org.apache.shiro.web.filter.authc

.AnonymousFilter

匿名拦截器,即不需要登录即可访问;一般用于静态资源过滤;示例“/static/**=anon”

授权相关的

roles

org.apache.shiro.web.filter.authz

.RolesAuthorizationFilter

角色授权拦截器,验证用户是否拥有所有角色;主要属性: loginUrl:登录页面地址(/login.jsp);unauthorizedUrl:未授权后重定向的地址;示例“/admin/**=roles[admin]”

perms

org.apache.shiro.web.filter.authz

.PermissionsAuthorizationFilter

权限授权拦截器,验证用户是否拥有所有权限;属性和roles一样;示例“/user/**=perms["user:create"]”

port

org.apache.shiro.web.filter.authz

.PortFilter

端口拦截器,主要属性:port(80):可以通过的端口;示例“/test= port[80]”,如果用户访问该页面是非80,将自动将请求端口改为80并重定向到该80端口,其他路径/参数等都一样

rest

org.apache.shiro.web.filter.authz

.HttpMethodPermissionFilter

rest风格拦截器,自动根据请求方法构建权限字符串(GET=read, POST=create,PUT=update,DELETE=delete,HEAD=read,TRACE=read,OPTIONS=read, MKCOL=create)构建权限字符串;示例“/users=rest[user]”,会自动拼出“user:read,user:create,user:update,user:delete”权限字符串进行权限匹配(所有都得匹配,isPermittedAll);

ssl

org.apache.shiro.web.filter.authz

.SslFilter

SSL拦截器,只有请求协议是https才能通过;否则自动跳转会https端口(443);其他和port拦截器一样;

其他

noSessionCreation

org.apache.shiro.web.filter.session

.NoSessionCreationFilter

不创建会话拦截器,调用 subject.getSession(false)不会有什么问题,但是如果 subject.getSession(true)将抛出 DisabledSessionException异常;

另外还提供了一个org.apache.shiro.web.filter.authz.HostFilter,即主机拦截器,比如其提供了属性:authorizedIps:已授权的ip地址,deniedIps:表示拒绝的ip地址;不过目前还没有完全实现,不可用。

这些默认的拦截器会自动注册,可以直接在ini配置文件中通过“拦截器名.属性”设置其属性:

  1. perms.unauthorizedUrl=/unauthorized

另外如果某个拦截器不想使用了可以直接通过如下配置直接禁用:

  1. perms.enabled=false

第九章 JSP标签——《跟我学Shiro》

Shiro提供了JSTL标签用于在JSP/GSP页面进行权限控制,如根据登录用户显示相应的页面按钮。

导入标签库

  1. <%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>

标签库定义在shiro-web.jar包下的META-INF/shiro.tld中定义。

guest标签

  1. <shiro:guest>
  2. 欢迎游客访问,<a href="${pageContext.request.contextPath}/login.jsp">登录</a>
  3. </shiro:guest>

用户没有身份验证时显示相应信息,即游客访问信息。

user标签

  1. <shiro:user>
  2. 欢迎[<shiro:principal/>]登录,<a href="${pageContext.request.contextPath}/logout">退出</a>
  3. </shiro:user>

用户已经身份验证/记住我登录后显示相应的信息。

authenticated标签

  1. <shiro:authenticated>
  2. 用户[<shiro:principal/>]已身份验证通过
  3. </shiro:authenticated>

用户已经身份验证通过,即Subject.login登录成功,不是记住我登录的。

notAuthenticated标签

<shiro:notAuthenticated>
未身份验证(包括记住我)
</shiro:notAuthenticated> 

用户已经身份验证通过,即没有调用Subject.login进行登录,包括记住我自动登录的也属于未进行身份验证。

principal标签

<shiro: principal/>

显示用户身份信息,默认调用Subject.getPrincipal()获取,即Primary Principal。

  1. <shiro:principal type="java.lang.String"/>

相当于Subject.getPrincipals().oneByType(String.class)。

  1. <shiro:principal type="java.lang.String"/>

相当于Subject.getPrincipals().oneByType(String.class)。

  1. <shiro:principal property="username"/>

相当于((User)Subject.getPrincipals()).getUsername()。

hasRole标签

  1. <shiro:hasRole name="admin">
  2. 用户[<shiro:principal/>]拥有角色admin<br/>
  3. </shiro:hasRole>

如果当前Subject有角色将显示body体内容。

hasAnyRoles标签

  1. <shiro:hasAnyRoles name="admin,user">
  2. 用户[<shiro:principal/>]拥有角色admin或user<br/>
  3. </shiro:hasAnyRoles>

如果当前Subject有任意一个角色(或的关系)将显示body体内容。

lacksRole标签

  1. <shiro:lacksRole name="abc">
  2. 用户[<shiro:principal/>]没有角色abc<br/>
  3. </shiro:lacksRole>

如果当前Subject没有角色将显示body体内容。

hasPermission标签

  1. <shiro:hasPermission name="user:create">
  2. 用户[<shiro:principal/>]拥有权限user:create<br/>
  3. </shiro:hasPermission>

如果当前Subject有权限将显示body体内容。

lacksPermission标签

  1. <shiro:lacksPermission name="org:create">
  2. 用户[<shiro:principal/>]没有权限org:create<br/>
  3. </shiro:lacksPermission>

如果当前Subject没有权限将显示body体内容。

另外又提供了几个权限控制相关的标签:

导入自定义标签库

  1. <%@taglib prefix="zhang" tagdir="/WEB-INF/tags" %>

示例

  1. <zhang:hasAllRoles name="admin,user">
  2. 用户[<shiro:principal/>]拥有角色admin和user<br/>
  3. </zhang:hasAllRoles>
  4. <zhang:hasAllPermissions name="user:create,user:update">
  5. 用户[<shiro:principal/>]拥有权限user:create和user:update<br/>
  6. </zhang:hasAllPermissions>
  7. <zhang:hasAnyPermissions name="user:create,abc:update">
  8. 用户[<shiro:principal/>]拥有权限user:create或abc:update<br/>
  9. </zhang:hasAnyPermissions>

hasAllRoles表示拥有所有相关的角色;hasAllPermissions表示拥有所有相关的权限;hasAnyPermissions表示拥有任意一个相关的权限。

第十章 会话管理——《跟我学Shiro》

Shiro提供了完整的企业级会话管理功能,不依赖于底层容器(如web容器tomcat),不管JavaSE还是JavaEE环境都可以使用,提供了会话管理、会话事件监听、会话存储/持久化、容器无关的集群、失效/过期支持、对Web的透明支持、SSO单点登录的支持等特性。即直接使用Shiro的会话管理可以直接替换如Web容器的会话管理。

会话

所谓会话,即用户访问应用时保持的连接关系,在多次交互中应用能够识别出当前访问的用户是谁,且可以在多次交互中保存一些数据。如访问一些网站时登录成功后,网站可以记住用户,且在退出之前都可以识别当前用户是谁。

Shiro的会话支持不仅可以在普通的JavaSE应用中使用,也可以在JavaEE应用中使用,如web应用。且使用方式是一致的。

  1. login("classpath:shiro.ini", "zhang", "123");
  2. Subject subject = SecurityUtils.getSubject();
  3. Session session = subject.getSession();

登录成功后使用Subject.getSession()即可获取会话;其等价于Subject.getSession(true),即如果当前没有创建Session对象会创建一个;另外Subject.getSession(false),如果当前没有创建Session则返回null(不过默认情况下如果启用会话存储功能的话在创建Subject时会主动创建一个Session)。

  1. session.getId();

获取当前会话的唯一标识。

  1. session.getHost();

获取当前Subject的主机地址,该地址是通过HostAuthenticationToken.getHost()提供的。

  1. session.getTimeout();
  2. session.setTimeout(毫秒);

获取/设置当前Session的过期时间;如果不设置默认是会话管理器的全局过期时间。

  1. session.getStartTimestamp();
  2. session.getLastAccessTime();

获取会话的启动时间及最后访问时间;如果是JavaSE应用需要自己定期调用session.touch()去更新最后访问时间;如果是Web应用,每次进入ShiroFilter都会自动调用session.touch()来更新最后访问时间。

  1. session.touch();
  2. session.stop();

更新会话最后访问时间及销毁会话;当Subject.logout()时会自动调用stop方法来销毁会话。如果在web中,调用javax.servlet.http.HttpSession. invalidate()也会自动调用Shiro Session.stop方法进行销毁Shiro的会话。

  1. session.setAttribute("key", "123");
  2. Assert.assertEquals("123", session.getAttribute("key"));
  3. session.removeAttribute("key");

设置/获取/删除会话属性;在整个会话范围内都可以对这些属性进行操作。

Shiro提供的会话可以用于JavaSE/JavaEE环境,不依赖于任何底层容器,可以独立使用,是完整的会话模块。

会话管理器

会话管理器管理着应用中所有Subject的会话的创建、维护、删除、失效、验证等工作。是Shiro的核心组件,顶层组件SecurityManager直接继承了SessionManager,且提供了SessionsSecurityManager实现直接把会话管理委托给相应的SessionManager,DefaultSecurityManager及DefaultWebSecurityManager默认SecurityManager都继承了SessionsSecurityManager。

SecurityManager提供了如下接口:

  1. Session start(SessionContext context); //启动会话
  2. Session getSession(SessionKey key) throws SessionException; //根据会话Key获取会话

另外用于Web环境的WebSessionManager又提供了如下接口:

  1. boolean isServletContainerSessions();//是否使用Servlet容器的会话

Shiro还提供了ValidatingSessionManager用于验资并过期会话:

  1. void validateSessions();//验证所有会话是否过期

Shiro提供了三个默认实现:

DefaultSessionManager:DefaultSecurityManager使用的默认实现,用于JavaSE环境;

ServletContainerSessionManager:DefaultWebSecurityManager使用的默认实现,用于Web环境,其直接使用Servlet容器的会话;

DefaultWebSessionManager:用于Web环境的实现,可以替代ServletContainerSessionManager,自己维护着会话,直接废弃了Servlet容器的会话管理。

替换SecurityManager默认的SessionManager可以在ini中配置(shiro.ini):

  1. [main]
  2. sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager
  3. securityManager.sessionManager=$sessionManager

Web环境下的ini配置(shiro-web.ini):

<!--EndFragment-->

  1. [main]
  2. sessionManager=org.apache.shiro.web.session.mgt.ServletContainerSessionManager
  3. securityManager.sessionManager=$sessionManager

另外可以设置会话的全局过期时间(毫秒为单位),默认30分钟:

  1. sessionManager. globalSessionTimeout=1800000

默认情况下globalSessionTimeout将应用给所有Session。可以单独设置每个Session的timeout属性来为每个Session设置其超时时间。

另外如果使用ServletContainerSessionManager进行会话管理,Session的超时依赖于底层Servlet容器的超时时间,可以在web.xml中配置其会话的超时时间(分钟为单位):

  1. <session-config>
  2. <session-timeout>30</session-timeout>
  3. </session-config>

在Servlet容器中,默认使用JSESSIONID Cookie维护会话,且会话默认是跟容器绑定的;在某些情况下可能需要使用自己的会话机制,此时我们可以使用DefaultWebSessionManager来维护会话:

  1. sessionIdCookie=org.apache.shiro.web.servlet.SimpleCookie
  2. sessionManager=org.apache.shiro.web.session.mgt.DefaultWebSessionManager
  3. sessionIdCookie.name=sid
  4. #sessionIdCookie.domain=
  5. #sessionIdCookie.path=
  6. sessionIdCookie.maxAge=1800
  7. sessionIdCookie.httpOnly=true
  8. sessionManager.sessionIdCookie=$sessionIdCookie
  9. sessionManager.sessionIdCookieEnabled=true
  10. securityManager.sessionManager=$sessionManager

sessionIdCookie是sessionManager创建会话Cookie的模板:

sessionIdCookie.name:设置Cookie名字,默认为JSESSIONID;

sessionIdCookie.domain:设置Cookie的域名,默认空,即当前访问的域名;

sessionIdCookie.path:设置Cookie的路径,默认空,即存储在域名根下;

sessionIdCookie.maxAge:设置Cookie的过期时间,秒为单位,默认-1表示关闭浏览器时过期Cookie;

sessionIdCookie.httpOnly:如果设置为true,则客户端不会暴露给客户端脚本代码,使用HttpOnly cookie有助于减少某些类型的跨站点脚本攻击;此特性需要实现了Servlet 2.5 MR6及以上版本的规范的Servlet容器支持;

sessionManager.sessionIdCookieEnabled:是否启用/禁用Session Id Cookie,默认是启用的;如果禁用后将不会设置Session Id Cookie,即默认使用了Servlet容器的JSESSIONID,且通过URL重写(URL中的“;JSESSIONID=id”部分)保存Session Id。

另外我们可以如“sessionManager. sessionIdCookie.name=sid”这种方式操作Cookie模板。

会话监听器

会话监听器用于监听会话创建、过期及停止事件:

  1. public class MySessionListener1 implements SessionListener {
  2. @Override
  3. public void onStart(Session session) {//会话创建时触发
  4. System.out.println("会话创建:" + session.getId());
  5. }
  6. @Override
  7. public void onExpiration(Session session) {//会话过期时触发
  8. System.out.println("会话过期:" + session.getId());
  9. }
  10. @Override
  11. public void onStop(Session session) {//退出/会话过期时触发
  12. System.out.println("会话停止:" + session.getId());
  13. }
  14. }

如果只想监听某一个事件,可以继承SessionListenerAdapter实现:

  1. public class MySessionListener2 extends SessionListenerAdapter {
  2. @Override
  3. public void onStart(Session session) {
  4. System.out.println("会话创建:" + session.getId());
  5. }
  6. }

在shiro-web.ini配置文件中可以进行如下配置设置会话监听器:

  1. sessionListener1=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener1
  2. sessionListener2=com.github.zhangkaitao.shiro.chapter10.web.listener.MySessionListener2
  3. sessionManager.sessionListeners=$sessionListener1,$sessionListener2

会话存储/持久化

Shiro提供SessionDAO用于会话的CRUD,即DAO(Data Access Object)模式实现:

  1. //如DefaultSessionManager在创建完session后会调用该方法;如保存到关系数据库/文件系统/NoSQL数据库;即可以实现会话的持久化;返回会话ID;主要此处返回的ID.equals(session.getId());
  2. Serializable create(Session session);
  3. //根据会话ID获取会话
  4. Session readSession(Serializable sessionId) throws UnknownSessionException;
  5. //更新会话;如更新会话最后访问时间/停止会话/设置超时时间/设置移除属性等会调用
  6. void update(Session session) throws UnknownSessionException;
  7. //删除会话;当会话过期/会话停止(如用户退出时)会调用
  8. void delete(Session session);
  9. //获取当前所有活跃用户,如果用户量多此方法影响性能
  10. Collection<Session> getActiveSessions();

Shiro内嵌了如下SessionDAO实现:

AbstractSessionDAO提供了SessionDAO的基础实现,如生成会话ID等;CachingSessionDAO提供了对开发者透明的会话缓存的功能,只需要设置相应的CacheManager即可;MemorySessionDAO直接在内存中进行会话维护;而EnterpriseCacheSessionDAO提供了缓存功能的会话维护,默认情况下使用MapCache实现,内部使用ConcurrentHashMap保存缓存的会话。

可以通过如下配置设置SessionDAO:

  1. sessionDAO=org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
  2. sessionManager.sessionDAO=$sessionDAO

Shiro提供了使用Ehcache进行会话存储,Ehcache可以配合TerraCotta实现容器无关的分布式集群。

首先在pom.xml里添加如下依赖:

  1. <dependency>
  2. <groupId>org.apache.shiro</groupId>
  3. <artifactId>shiro-ehcache</artifactId>
  4. <version>1.2.2</version>
  5. </dependency>

接着配置shiro-web.ini文件:

  1. sessionDAO=org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO
  2. sessionDAO. activeSessionsCacheName=shiro-activeSessionCache
  3. sessionManager.sessionDAO=$sessionDAO
  4. cacheManager = org.apache.shiro.cache.ehcache.EhCacheManager
  5. cacheManager.cacheManagerConfigFile=classpath:ehcache.xml
  6. securityManager.cacheManager = $cacheManager

sessionDAO. activeSessionsCacheName:设置Session缓存名字,默认就是shiro-activeSessionCache;

cacheManager:缓存管理器,用于管理缓存的,此处使用Ehcache实现;

cacheManager.cacheManagerConfigFile:设置ehcache缓存的配置文件;

securityManager.cacheManager:设置SecurityManager的cacheManager,会自动设置实现了CacheManagerAware接口的相应对象,如SessionDAO的cacheManager;

然后配置ehcache.xml:

  1. <cache name="shiro-activeSessionCache"
  2. maxEntriesLocalHeap="10000"
  3. overflowToDisk="false"
  4. eternal="false"
  5. diskPersistent="false"
  6. timeToLiveSeconds="0"
  7. timeToIdleSeconds="0"
  8. statistics="true"/>

Cache的名字为shiro-activeSessionCache,即设置的sessionDAO的activeSessionsCacheName属性值。

另外可以通过如下ini配置设置会话ID生成器:

  1. sessionIdGenerator=org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator
  2. sessionDAO.sessionIdGenerator=$sessionIdGenerator

用于生成会话ID,默认就是JavaUuidSessionIdGenerator,使用java.util.UUID生成。

如果自定义实现SessionDAO,继承CachingSessionDAO即可:

  1. public class MySessionDAO extends CachingSessionDAO {
  2. private JdbcTemplate jdbcTemplate = JdbcTemplateUtils.jdbcTemplate();
  3. protected Serializable doCreate(Session session) {
  4. Serializable sessionId = generateSessionId(session);
  5. assignSessionId(session, sessionId);
  6. String sql = "insert into sessions(id, session) values(?,?)";
  7. jdbcTemplate.update(sql, sessionId, SerializableUtils.serialize(session));
  8. return session.getId();
  9. }
  10. protected void doUpdate(Session session) {
  11. if(session instanceof ValidatingSession && !((ValidatingSession)session).isValid()) {
  12. return; //如果会话过期/停止 没必要再更新了
  13. }
  14. String sql = "update sessions set session=? where id=?";
  15. jdbcTemplate.update(sql, SerializableUtils.serialize(session), session.getId());
  16. }
  17. protected void doDelete(Session session) {
  18. String sql = "delete from sessions where id=?";
  19. jdbcTemplate.update(sql, session.getId());
  20. }
  21. protected Session doReadSession(Serializable sessionId) {
  22. String sql = "select session from sessions where id=?";
  23. List<String> sessionStrList = jdbcTemplate.queryForList(sql, String.class, sessionId);
  24. if(sessionStrList.size() == 0) return null;
  25. return SerializableUtils.deserialize(sessionStrList.get(0));
  26. }
  27. }

doCreate/doUpdate/doDelete/doReadSession分别代表创建/修改/删除/读取会话;此处通过把会话序列化后存储到数据库实现;接着在shiro-web.ini中配置:

  1. sessionDAO=com.github.zhangkaitao.shiro.chapter10.session.dao.MySessionDAO

其他设置和之前一样,因为继承了CachingSessionDAO;所有在读取时会先查缓存中是否存在,如果找不到才到数据库中查找。

会话验证

Shiro提供了会话验证调度器,用于定期的验证会话是否已过期,如果过期将停止会话;出于性能考虑,一般情况下都是获取会话时来验证会话是否过期并停止会话的;但是如在web环境中,如果用户不主动退出是不知道会话是否过期的,因此需要定期的检测会话是否过期,Shiro提供了会话验证调度器SessionValidationScheduler来做这件事情。

可以通过如下ini配置开启会话验证:

  1. sessionValidationScheduler=org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler
  2. sessionValidationScheduler.interval = 3600000
  3. sessionValidationScheduler.sessionManager=$sessionManager
  4. sessionManager.globalSessionTimeout=1800000
  5. sessionManager.sessionValidationSchedulerEnabled=true
  6. sessionManager.sessionValidationScheduler=$sessionValidationScheduler

sessionValidationScheduler:会话验证调度器,sessionManager默认就是使用ExecutorServiceSessionValidationScheduler,其使用JDK的ScheduledExecutorService进行定期调度并验证会话是否过期;

sessionValidationScheduler.interval:设置调度时间间隔,单位毫秒,默认就是1小时;

sessionValidationScheduler.sessionManager:设置会话验证调度器进行会话验证时的会话管理器;

sessionManager.globalSessionTimeout:设置全局会话超时时间,默认30分钟,即如果30分钟内没有访问会话将过期;

sessionManager.sessionValidationSchedulerEnabled:是否开启会话验证器,默认是开启的;

sessionManager.sessionValidationScheduler:设置会话验证调度器,默认就是使用ExecutorServiceSessionValidationScheduler。

Shiro也提供了使用Quartz会话验证调度器:

  1. sessionValidationScheduler=org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler
  2. sessionValidationScheduler.sessionValidationInterval = 3600000
  3. sessionValidationScheduler.sessionManager=$sessionManager

使用时需要导入shiro-quartz依赖:

  1. <dependency>
  2. <groupId>org.apache.shiro</groupId>
  3. <artifactId>shiro-quartz</artifactId>
  4. <version>1.2.2</version>
  5. </dependency>

如上会话验证调度器实现都是直接调用AbstractValidatingSessionManager 的validateSessions方法进行验证,其直接调用SessionDAO的getActiveSessions方法获取所有会话进行验证,如果会话比较多,会影响性能;可以考虑如分页获取会话并进行验证,如com.github.zhangkaitao.shiro.chapter10.session.scheduler.MySessionValidationScheduler:

  1. //分页获取会话并验证
  2. String sql = "select session from sessions limit ?,?";
  3. int start = 0; //起始记录
  4. int size = 20; //每页大小
  5. List<String> sessionList = jdbcTemplate.queryForList(sql, String.class, start, size);
  6. while(sessionList.size() > 0) {
  7. for(String sessionStr : sessionList) {
  8. try {
  9. Session session = SerializableUtils.deserialize(sessionStr);
  10. Method validateMethod =
  11. ReflectionUtils.findMethod(AbstractValidatingSessionManager.class,
  12. "validate", Session.class, SessionKey.class);
  13. validateMethod.setAccessible(true);
  14. ReflectionUtils.invokeMethod(validateMethod,
  15. sessionManager, session, new DefaultSessionKey(session.getId()));
  16. } catch (Exception e) {
  17. //ignore
  18. }
  19. }
  20. start = start + size;
  21. sessionList = jdbcTemplate.queryForList(sql, String.class, start, size);
  22. }

其直接改造自ExecutorServiceSessionValidationScheduler,如上代码是验证的核心代码,可以根据自己的需求改造此验证调度器器;ini的配置和之前的类似。

如果在会话过期时不想删除过期的会话,可以通过如下ini配置进行设置:

  1. sessionManager.deleteInvalidSessions=false

默认是开启的,在会话过期后会调用SessionDAO的delete方法删除会话:如会话时持久化存储的,可以调用此方法进行删除。

如果是在获取会话时验证了会话已过期,将抛出InvalidSessionException;因此需要捕获这个异常并跳转到相应的页面告诉用户会话已过期,让其重新登录,如可以在web.xml配置相应的错误页面:

  1. <error-page>
  2. <exception-type>org.apache.shiro.session.InvalidSessionException</exception-type>
  3. <location>/invalidSession.jsp</location>
  4. </error-page>

sessionFactory

sessionFactory是创建会话的工厂,根据相应的Subject上下文信息来创建会话;默认提供了SimpleSessionFactory用来创建SimpleSession会话。

首先自定义一个Session:

  1. public class OnlineSession extends SimpleSession {
  2. public static enum OnlineStatus {
  3. on_line("在线"), hidden("隐身"), force_logout("强制退出");
  4. private final String info;
  5. private OnlineStatus(String info) {
  6. this.info = info;
  7. }
  8. public String getInfo() {
  9. return info;
  10. }
  11. }
  12. private String userAgent; //用户浏览器类型
  13. private OnlineStatus status = OnlineStatus.on_line; //在线状态
  14. private String systemHost; //用户登录时系统IP
  15. //省略其他
  16. }

OnlineSession用于保存当前登录用户的在线状态,支持如离线等状态的控制。

接着自定义SessionFactory:

  1. public class OnlineSessionFactory implements SessionFactory {
  2. @Override
  3. public Session createSession(SessionContext initData) {
  4. OnlineSession session = new OnlineSession();
  5. if (initData != null && initData instanceof WebSessionContext) {
  6. WebSessionContext sessionContext = (WebSessionContext) initData;
  7. HttpServletRequest request = (HttpServletRequest) sessionContext.getServletRequest();
  8. if (request != null) {
  9. session.setHost(IpUtils.getIpAddr(request));
  10. session.setUserAgent(request.getHeader("User-Agent"));
  11. session.setSystemHost(request.getLocalAddr() + ":" + request.getLocalPort());
  12. }
  13. }
  14. return session;
  15. }
  16. }

根据会话上下文创建相应的OnlineSession。

最后在shiro-web.ini配置文件中配置:

  1. sessionFactory=org.apache.shiro.session.mgt.OnlineSessionFactory
  2. sessionManager.sessionFactory=$sessionFactory

第十一章 缓存机制——《跟我学Shiro》

Shiro提供了类似于Spring的Cache抽象,即Shiro本身不实现Cache,但是对Cache进行了又抽象,方便更换不同的底层Cache实现。对于Cache的一些概念可以参考我的《Spring Cache抽象详解》:http://jinnianshilongnian.iteye.com/blog/2001040

Shiro提供的Cache接口:

  1. public interface Cache<K, V> {
  2. //根据Key获取缓存中的值
  3. public V get(K key) throws CacheException;
  4. //往缓存中放入key-value,返回缓存中之前的值
  5. public V put(K key, V value) throws CacheException;
  6. //移除缓存中key对应的值,返回该值
  7. public V remove(K key) throws CacheException;
  8. //清空整个缓存
  9. public void clear() throws CacheException;
  10. //返回缓存大小
  11. public int size();
  12. //获取缓存中所有的key
  13. public Set<K> keys();
  14. //获取缓存中所有的value
  15. public Collection<V> values();
  16. }

Shiro提供的CacheManager接口:

  1. public interface CacheManager {
  2. //根据缓存名字获取一个Cache
  3. public <K, V> Cache<K, V> getCache(String name) throws CacheException;
  4. }

Shiro还提供了CacheManagerAware用于注入CacheManager

  1. public interface CacheManagerAware {
  2. //注入CacheManager
  3. void setCacheManager(CacheManager cacheManager);
  4. }

Shiro内部相应的组件(DefaultSecurityManager)会自动检测相应的对象(如Realm)是否实现了CacheManagerAware并自动注入相应的CacheManager。

本章用例使用了与第六章的代码。

Realm缓存

Shiro提供了CachingRealm,其实现了CacheManagerAware接口,提供了缓存的一些基础实现;另外AuthenticatingRealm及AuthorizingRealm分别提供了对AuthenticationInfo 和AuthorizationInfo信息的缓存。

ini配置

  1. userRealm=com.github.zhangkaitao.shiro.chapter11.realm.UserRealm
  2. userRealm.credentialsMatcher=$credentialsMatcher
  3. userRealm.cachingEnabled=true
  4. userRealm.authenticationCachingEnabled=true
  5. userRealm.authenticationCacheName=authenticationCache
  6. userRealm.authorizationCachingEnabled=true
  7. userRealm.authorizationCacheName=authorizationCache
  8. securityManager.realms=$userRealm
  9. cacheManager=org.apache.shiro.cache.ehcache.EhCacheManager
  10. cacheManager.cacheManagerConfigFile=classpath:shiro-ehcache.xml
  11. securityManager.cacheManager=$cacheManager

userRealm.cachingEnabled:启用缓存,默认false;

userRealm.authenticationCachingEnabled:启用身份验证缓存,即缓存AuthenticationInfo信息,默认false;

userRealm.authenticationCacheName:缓存AuthenticationInfo信息的缓存名称;

userRealm. authorizationCachingEnabled:启用授权缓存,即缓存AuthorizationInfo信息,默认false;

userRealm. authorizationCacheName:缓存AuthorizationInfo信息的缓存名称;

cacheManager:缓存管理器,此处使用EhCacheManager,即Ehcache实现,需要导入相应的Ehcache依赖,请参考pom.xml;

因为测试用例的关系,需要将Ehcache的CacheManager改为使用VM单例模式:

this.manager = new net.sf.ehcache.CacheManager(getCacheManagerConfigFileInputStream());

改为

this.manager = net.sf.ehcache.CacheManager.create(getCacheManagerConfigFileInputStream());

测试用例

  1. @Test
  2. public void testClearCachedAuthenticationInfo() {
  3. login(u1.getUsername(), password);
  4. userService.changePassword(u1.getId(), password + "1");
  5. RealmSecurityManager securityManager =
  6. (RealmSecurityManager) SecurityUtils.getSecurityManager();
  7. UserRealm userRealm = (UserRealm) securityManager.getRealms().iterator().next();
  8. userRealm.clearCachedAuthenticationInfo(subject().getPrincipals());
  9. login(u1.getUsername(), password + "1");
  10. }

首先登录成功(此时会缓存相应的AuthenticationInfo),然后修改密码;此时密码就变了;接着需要调用Realm的clearCachedAuthenticationInfo方法清空之前缓存的AuthenticationInfo;否则下次登录时还会获取到修改密码之前的那个AuthenticationInfo;

  1. @Test
  2. public void testClearCachedAuthorizationInfo() {
  3. login(u1.getUsername(), password);
  4. subject().checkRole(r1.getRole());
  5. userService.correlationRoles(u1.getId(), r2.getId());
  6. RealmSecurityManager securityManager =
  7. (RealmSecurityManager) SecurityUtils.getSecurityManager();
  8. UserRealm userRealm = (UserRealm)securityManager.getRealms().iterator().next();
  9. userRealm.clearCachedAuthorizationInfo(subject().getPrincipals());
  10. subject().checkRole(r2.getRole());
  11. }

和之前的用例差不多;此处调用Realm的clearCachedAuthorizationInfo清空之前缓存的AuthorizationInfo;

另外还有clearCache,其同时调用clearCachedAuthenticationInfo和clearCachedAuthorizationInfo,清空AuthenticationInfo和AuthorizationInfo。

UserRealm还提供了clearAllCachedAuthorizationInfo、clearAllCachedAuthenticationInfo、clearAllCache,用于清空整个缓存。

在某些清空下这种方式可能不是最好的选择,可以考虑直接废弃Shiro的缓存,然后自己通过如AOP机制实现自己的缓存;可以参考:

另外如果和Spring集成时可以考虑直接使用Spring的Cache抽象,可以考虑使用SpringCacheManagerWrapper,其对Spring Cache进行了包装,转换为Shiro的CacheManager实现:

https://github.com/zhangkaitao/es/blob/master/web/src/main/java/org/apache/shiro/cache/spring/SpringCacheManagerWrapper.java

Session缓存

当我们设置了SecurityManager的CacheManager时,如:

  1. securityManager.cacheManager=$cacheManager

当我们设置SessionManager时:

  1. sessionManager=org.apache.shiro.session.mgt.DefaultSessionManager
  2. securityManager.sessionManager=$sessionManager

如securityManager实现了SessionsSecurityManager,其会自动判断SessionManager是否实现了CacheManagerAware接口,如果实现了会把CacheManager设置给它。然后sessionManager会判断相应的sessionDAO(如继承自CachingSessionDAO)是否实现了CacheManagerAware,如果实现了会把CacheManager设置给它;如第九章的MySessionDAO就是带缓存的SessionDAO;其会先查缓存,如果找不到才查数据库。

对于CachingSessionDAO,可以通过如下配置设置缓存的名称:

  1. sessionDAO=com.github.zhangkaitao.shiro.chapter11.session.dao.MySessionDAO
  2. sessionDAO.activeSessionsCacheName=shiro-activeSessionCache

activeSessionsCacheName默认就是shiro-activeSessionCache。

第十二章 与Spring集成——《跟我学Shiro》

Shiro的组件都是JavaBean/POJO式的组件,所以非常容易使用Spring进行组件管理,可以非常方便的从ini配置迁移到Spring进行管理,且支持JavaSE应用及Web应用的集成。

在示例之前,需要导入shiro-spring及spring-context依赖,具体请参考pom.xml。

spring-beans.xml配置文件提供了基础组件如DataSource、DAO、Service组件的配置。

JavaSE应用

spring-shiro.xml提供了普通JavaSE独立应用的Spring配置:

  1. <!-- 缓存管理器 使用Ehcache实现 -->
  2. <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager">
  3. <property name="cacheManagerConfigFile" value="classpath:ehcache.xml"/>
  4. </bean>
  5. <!-- 凭证匹配器 -->
  6. <bean id="credentialsMatcher" class="
  7. com.github.zhangkaitao.shiro.chapter12.credentials.RetryLimitHashedCredentialsMatcher">
  8. <constructor-arg ref="cacheManager"/>
  9. <property name="hashAlgorithmName" value="md5"/>
  10. <property name="hashIterations" value="2"/>
  11. <property name="storedCredentialsHexEncoded" value="true"/>
  12. </bean>
  13. <!-- Realm实现 -->
  14. <bean id="userRealm" class="com.github.zhangkaitao.shiro.chapter12.realm.UserRealm">
  15. <property name="userService" ref="userService"/>
  16. <property name="credentialsMatcher" ref="credentialsMatcher"/>
  17. <property name="cachingEnabled" value="true"/>
  18. <property name="authenticationCachingEnabled" value="true"/>
  19. <property name="authenticationCacheName" value="authenticationCache"/>
  20. <property name="authorizationCachingEnabled" value="true"/>
  21. <property name="authorizationCacheName" value="authorizationCache"/>
  22. </bean>
  23. <!-- 会话ID生成器 -->
  24. <bean id="sessionIdGenerator"
  25. class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/>
  26. <!-- 会话DAO -->
  27. <bean id="sessionDAO"
  28. class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO">
  29. <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/>
  30. <property name="sessionIdGenerator" ref="sessionIdGenerator"/>
  31. </bean>
  32. <!-- 会话验证调度器 -->
  33. <bean id="sessionValidationScheduler"
  34. class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler">
  35. <property name="sessionValidationInterval" value="1800000"/>
  36. <property name="sessionManager" ref="sessionManager"/>
  37. </bean>
  38. <!-- 会话管理器 -->
  39. <bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager">
  40. <property name="globalSessionTimeout" value="1800000"/>
  41. <property name="deleteInvalidSessions" value="true"/>
  42. <property name="sessionValidationSchedulerEnabled" value="true"/>
  43. <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
  44. <property name="sessionDAO" ref="sessionDAO"/>
  45. </bean>
  46. <!-- 安全管理器 -->
  47. <bean id="securityManager" class="org.apache.shiro.mgt.DefaultSecurityManager">
  48. <property name="realms">
  49. <list><ref bean="userRealm"/></list>
  50. </property>
  51. <property name="sessionManager" ref="sessionManager"/>
  52. <property name="cacheManager" ref="cacheManager"/>
  53. </bean>
  54. <!-- 相当于调用SecurityUtils.setSecurityManager(securityManager) -->
  55. <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
  56. <property name="staticMethod"
  57. value="org.apache.shiro.SecurityUtils.setSecurityManager"/>
  58. <property name="arguments" ref="securityManager"/>
  59. </bean>
  60. <!-- Shiro生命周期处理器-->
  61. <bean id="lifecycleBeanPostProcessor"
  62. class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>

可以看出,只要把之前的ini配置翻译为此处的spring xml配置方式即可,无须多解释。LifecycleBeanPostProcessor用于在实现了Initializable接口的Shiro bean初始化时调用Initializable接口回调,在实现了Destroyable接口的Shiro bean销毁时调用 Destroyable接口回调。如UserRealm就实现了Initializable,而DefaultSecurityManager实现了Destroyable。具体可以查看它们的继承关系。

测试用例请参考com.github.zhangkaitao.shiro.chapter12.ShiroTest。

Web应用

Web应用和普通JavaSE应用的某些配置是类似的,此处只提供一些不一样的配置,详细配置可以参考spring-shiro-web.xml。

  1. <!-- 会话Cookie模板 -->
  2. <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
  3. <constructor-arg value="sid"/>
  4. <property name="httpOnly" value="true"/>
  5. <property name="maxAge" value="180000"/>
  6. </bean>
  7. <!-- 会话管理器 -->
  8. <bean id="sessionManager"
  9. class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager">
  10. <property name="globalSessionTimeout" value="1800000"/>
  11. <property name="deleteInvalidSessions" value="true"/>
  12. <property name="sessionValidationSchedulerEnabled" value="true"/>
  13. <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/>
  14. <property name="sessionDAO" ref="sessionDAO"/>
  15. <property name="sessionIdCookieEnabled" value="true"/>
  16. <property name="sessionIdCookie" ref="sessionIdCookie"/>
  17. </bean>
  18. <!-- 安全管理器 -->
  19. <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
  20. <property name="realm" ref="userRealm"/>
  21. <property name="sessionManager" ref="sessionManager"/>
  22. <property name="cacheManager" ref="cacheManager"/>
  23. </bean>

1、sessionIdCookie是用于生产Session ID Cookie的模板;

2、会话管理器使用用于web环境的DefaultWebSessionManager;

3、安全管理器使用用于web环境的DefaultWebSecurityManager。

  1. <!-- 基于Form表单的身份验证过滤器 -->
  2. <bean id="formAuthenticationFilter"
  3. class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter">
  4. <property name="usernameParam" value="username"/>
  5. <property name="passwordParam" value="password"/>
  6. <property name="loginUrl" value="/login.jsp"/>
  7. </bean>
  8. <!-- Shiro的Web过滤器 -->
  9. <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
  10. <property name="securityManager" ref="securityManager"/>
  11. <property name="loginUrl" value="/login.jsp"/>
  12. <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
  13. <property name="filters">
  14. <util:map>
  15. <entry key="authc" value-ref="formAuthenticationFilter"/>
  16. </util:map>
  17. </property>
  18. <property name="filterChainDefinitions">
  19. <value>
  20. /index.jsp = anon
  21. /unauthorized.jsp = anon
  22. /login.jsp = authc
  23. /logout = logout
  24. /** = user
  25. </value>
  26. </property>
  27. </bean>

1、formAuthenticationFilter为基于Form表单的身份验证过滤器;此处可以再添加自己的Filter bean定义;

2、shiroFilter:此处使用ShiroFilterFactoryBean来创建ShiroFilter过滤器;filters属性用于定义自己的过滤器,即ini配置中的[filters]部分;filterChainDefinitions用于声明url和filter的关系,即ini配置中的[urls]部分。

接着需要在web.xml中进行如下配置:

  1. <context-param>
  2. <param-name>contextConfigLocation</param-name>
  3. <param-value>
  4. classpath:spring-beans.xml,
  5. classpath:spring-shiro-web.xml
  6. </param-value>
  7. </context-param>
  8. <listener>
  9. <listener-class>
  10. org.springframework.web.context.ContextLoaderListener
  11. </listener-class>
  12. </listener>

通过ContextLoaderListener加载contextConfigLocation指定的Spring配置文件。

  1. <filter>
  2. <filter-name>shiroFilter</filter-name>
  3. <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  4. <init-param>
  5. <param-name>targetFilterLifecycle</param-name>
  6. <param-value>true</param-value>
  7. </init-param>
  8. </filter>
  9. <filter-mapping>
  10. <filter-name>shiroFilter</filter-name>
  11. <url-pattern>/*</url-pattern>
  12. </filter-mapping>

DelegatingFilterProxy会自动到Spring容器中查找名字为shiroFilter的bean并把filter请求交给它处理。

其他配置请参考源代码。

Shiro权限注解

Shiro提供了相应的注解用于权限控制,如果使用这些注解就需要使用AOP的功能来进行判断,如Spring AOP;Shiro提供了Spring AOP集成用于权限注解的解析和验证。

为了测试,此处使用了Spring MVC来测试Shiro注解,当然Shiro注解不仅仅可以在web环境使用,在独立的JavaSE中也是可以用的,此处只是以web为例了。

在spring-mvc.xml配置文件添加Shiro Spring AOP权限注解的支持:

  1. <aop:config proxy-target-class="true"></aop:config>
  2. <bean class="
  3. org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
  4. <property name="securityManager" ref="securityManager"/>
  5. </bean>

如上配置用于开启Shiro Spring AOP权限注解的支持;<aop:config proxy-target-class="true">表示代理类。

接着就可以在相应的控制器(AnnotationController)中使用如下方式进行注解:

  1. @RequiresRoles("admin")
  2. @RequestMapping("/hello2")
  3. public String hello2() {
  4. return "success";
  5. }

访问hello2方法的前提是当前用户有admin角色。

当验证失败,其会抛出UnauthorizedException异常,此时可以使用Spring的ExceptionHandler(DefaultExceptionHandler)来进行拦截处理:

  1. @ExceptionHandler({UnauthorizedException.class})
  2. @ResponseStatus(HttpStatus.UNAUTHORIZED)
  3. public ModelAndView processUnauthenticatedException(NativeWebRequest request, UnauthorizedException e) {
  4. ModelAndView mv = new ModelAndView();
  5. mv.addObject("exception", e);
  6. mv.setViewName("unauthorized");
  7. return mv;
  8. }

如果集成Struts2,需要注意《Shiro+Struts2+Spring3 加上@RequiresPermissions 后@Autowired失效》问题:

http://jinnianshilongnian.iteye.com/blog/1850425

权限注解

  1. @RequiresAuthentication

表示当前Subject已经通过login进行了身份验证;即Subject. isAuthenticated()返回true。

  1. @RequiresUser

表示当前Subject已经身份验证或者通过记住我登录的。

  1. @RequiresGuest

表示当前Subject没有身份验证或通过记住我登录过,即是游客身份。

  1. @RequiresRoles(value={“admin”, “user”}, logical= Logical.AND)

表示当前Subject需要角色admin和user。

  1. @RequiresPermissions (value={“user:a”, “user:b”}, logical= Logical.OR)

表示当前Subject需要权限user:a或user:b。

第十三章 RememberMe——《跟我学Shiro》

Shiro提供了记住我(RememberMe)的功能,比如访问如淘宝等一些网站时,关闭了浏览器下次再打开时还是能记住你是谁,下次访问时无需再登录即可访问,基本流程如下:

1、首先在登录页面选中RememberMe然后登录成功;如果是浏览器登录,一般会把RememberMe的Cookie写到客户端并保存下来;

2、关闭浏览器再重新打开;会发现浏览器还是记住你的;

3、访问一般的网页服务器端还是知道你是谁,且能正常访问;

4、但是比如我们访问淘宝时,如果要查看我的订单或进行支付时,此时还是需要再进行身份认证的,以确保当前用户还是你。

RememberMe配置

spring-shiro-web.xml配置:

  1. <!-- 会话Cookie模板 -->
  2. <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
  3. <constructor-arg value="sid"/>
  4. <property name="httpOnly" value="true"/>
  5. <property name="maxAge" value="-1"/>
  6. </bean>
  7. <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
  8. <constructor-arg value="rememberMe"/>
  9. <property name="httpOnly" value="true"/>
  10. <property name="maxAge" value="2592000"/><!-- 30天 -->
  11. </bean>

sessionIdCookie:maxAge=-1表示浏览器关闭时失效此Cookie;

rememberMeCookie:即记住我的Cookie,保存时长30天;

  1. <!-- rememberMe管理器 -->
  2. <bean id="rememberMeManager"
  3. class="org.apache.shiro.web.mgt.CookieRememberMeManager">
  4. <property name="cipherKey" value="
  5. #{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/>
  6. <property name="cookie" ref="rememberMeCookie"/>
  7. </bean>

rememberMe管理器,cipherKey是加密rememberMe Cookie的密钥;默认AES算法;

  1. <!-- 安全管理器 -->
  2. <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
  3. ……
  4. <property name="rememberMeManager" ref="rememberMeManager"/>
  5. </bean>

设置securityManager安全管理器的rememberMeManager;

  1. <bean id="formAuthenticationFilter"
  2. class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter">
  3. ……
  4. <property name="rememberMeParam" value="rememberMe"/>
  5. </bean>

rememberMeParam,即rememberMe请求参数名,请求参数是boolean类型,true表示rememberMe。

  1. <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
  2. ……
  3. <property name="filterChainDefinitions">
  4. <value>
  5. /login.jsp = authc
  6. /logout = logout
  7. /authenticated.jsp = authc
  8. /** = user
  9. </value>
  10. </property>
  11. </bean>

“/authenticated.jsp = authc”表示访问该地址用户必须身份验证通过(Subject. isAuthenticated()==true);而“/** = user”表示访问该地址的用户是身份验证通过或RememberMe登录的都可以。

测试:

1、访问http://localhost:8080/chapter13/,会跳转到登录页面,登录成功后会设置会话及rememberMe Cookie;

2、关闭浏览器,此时会话cookie将失效;

3、然后重新打开浏览器访问http://localhost:8080/chapter13/,还是可以访问的;

4、如果此时访问http://localhost:8080/chapter13/authenticated.jsp,会跳转到登录页面重新进行身份验证。

如果要自己做RememeberMe,需要在登录之前这样创建Token:UsernamePasswordToken(用户名,密码,是否记住我),如:

  1. Subject subject = SecurityUtils.getSubject();
  2. UsernamePasswordToken token = new UsernamePasswordToken(username, password);
  3. token.setRememberMe(true);
  4. subject.login(token);

subject.isAuthenticated()表示用户进行了身份验证登录的,即使有Subject.login进行了登录;subject.isRemembered():表示用户是通过记住我登录的,此时可能并不是真正的你(如你的朋友使用你的电脑,或者你的cookie被窃取)在访问的;且两者二选一,即subject.isAuthenticated()==true,则subject.isRemembered()==false;反之一样。

另外对于过滤器,一般这样使用:

访问一般网页,如个人在主页之类的,我们使用user拦截器即可,user拦截器只要用户登录(isRemembered()==true or isAuthenticated()==true)过即可访问成功;

访问特殊网页,如我的订单,提交订单页面,我们使用authc拦截器即可,authc拦截器会判断用户是否是通过Subject.login(isAuthenticated()==true)登录的,如果是才放行,否则会跳转到登录页面叫你重新登录。

因此RememberMe使用过程中,需要配合相应的拦截器来实现相应的功能,用错了拦截器可能就不能满足你的需求了。

学Shiro完结版-3的更多相关文章

  1. 跟我学Shiro目录贴

    转发地址:https://www.iteye.com/blog/jinnianshilongnian-2018398 扫一扫,关注我的公众号 购买地址 历经三个月左右时间,<跟我学Shiro&g ...

  2. 【知识整理】这可能是最好的RxJava 2.x 教程(完结版)

    为什么要学 RxJava? 提升开发效率,降低维护成本一直是开发团队永恒不变的宗旨.近两年来国内的技术圈子中越来越多的开始提及 RxJava ,越来越多的应用和面试中都会有 RxJava ,而就目前的 ...

  3. 第十七章 OAuth2集成——《跟我学Shiro》

    目录贴:跟我学Shiro目录贴 目前很多开放平台如新浪微博开放平台都在使用提供开放API接口供开发者使用,随之带来了第三方应用要到开放平台进行授权的问题,OAuth就是干这个的,OAuth2是OAut ...

  4. 第一章 Shiro简介——《跟我学Shiro》(转)

    目录贴:跟我学Shiro目录贴 1.1  简介 Apache Shiro是Java的一个安全框架.目前,使用Apache Shiro的人越来越多,因为它相当简单,对比Spring Security,可 ...

  5. 跟开涛老师学shiro -- 编码/加密

    在涉及到密码存储问题上,应该加密/生成密码摘要存储,而不是存储明文密码.比如之前的600w csdn账号泄露对用户可能造成很大损失,因此应加密/生成不可逆的摘要方式存储. 5.1 编码/解码 Shir ...

  6. 跟开涛老师学shiro -- INI配置

    之前章节我们已经接触过一些INI配置规则了,如果大家使用过如spring之类的IoC/DI容器的话,Shiro提供的INI配置也是非常类似的,即可以理解为是一个IoC/DI容器,但是区别在于它从一个根 ...

  7. 跟开涛老师学shiro -- 授权

    授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等).在授权中需了解的几个关键对象:主体(Subject).资源(Resource).权限(Permission).角 ...

  8. 跟开涛老师学shiro -- 身份验证

    身份验证,即在应用中谁能证明他就是他本人.一般提供如他们的身份ID一些标识信息来表明他就是他本人,如提供身份证,用户名/密码来证明. 在shiro中,用户需要提供principals (身份)和cre ...

  9. 跟开涛老师学shiro -- shiro简介

    1.1  简介 Apache Shiro是Java的一个安全框架.目前,使用Apache Shiro的人越来越多,因为它相当简单,对比Spring Security,可能没有Spring Securi ...

  10. 【转】Android 开发规范(完结版)

    摘要 1 前言 2 AS 规范 3 命名规范 4 代码样式规范 5 资源文件规范 6 版本统一规范 7 第三方库规范 8 注释规范 9 测试规范 10 其他的一些规范 1 前言 为了有利于项目维护.增 ...

随机推荐

  1. 【CoCollider】让系统和应用适配如此简单

    在各平台应用开发过程中,随着业务的功能增加,不免会涉及到非公开的API依赖,针对某些应用或厂商系统的适配,每个版本都需要投入精力去排查,CoCollider 可以让我们的适配效率从几个星期提升到几小时 ...

  2. cnblogs内容同步到51cto上的说明(声明)

    51CTO网站上的blog地址为:https://blog.51cto.com/u_15642578 该地址是个人在博客园cnblogs上的同步账号(https://cnblogs.com/xyz), ...

  3. 15.Kubernetes持久化存储

    Kubernetes持久化存储 前言 之前我们有提到数据卷:emptydir ,是本地存储,pod重启,数据就不存在了,需要对数据持久化存储 对于数据持久化存储[pod重启,数据还存在],有两种方式 ...

  4. Bulk-Crap-Uninstaller:一个高效卸载,轻松管理你的应用程序的.Net开源工具

    我们在工作中,经常需要安装大量的软件,随着应用程序的不断增多,管理这些软件变得非常困难. 下面介绍一款具备高效.简洁的特点,可以帮助我们快速卸载大量不需要的应用程序,让电脑管理变得更加轻松. 01 项 ...

  5. PostgreSQL加密连接SSL配置

    PostgreSQL加密连接SSL配置 环境说明 操作系统 主机名 IP 类型 说明 CentOS Linux release 7.6.1810 (Core) centos7.6 192.168.1. ...

  6. 使用 JuiceFS 快照功能实现数据库发布与端到端测试

    今天的博客来自 JuiceFS 云服务用户 Jerry,他们通过使用 JuiceFS snapshot 功能,创新性地实现了数据的版本控制.Jerry,是一家位于北美的科技公司,利用人工智能和机器学习 ...

  7. 终于搞全了:GPIO/ADC/LED/I2C/SPI/USB…

    合宙低功耗4G模组经典型号Air780E,支持两种软件开发方式: 一种是传统的AT指令:一种是基于模组做Open开发. 传统AT指令的开发方式,合宙模组与行业内其它模组品牌在软件上区别不大,在硬件功耗 ...

  8. Deque的应用案例-回文检查

    7.Deque的应用案例-回文检查 回文检测:设计程序,检测一个字符串是否为回文. 回文:回文是一个字符串,读取首尾相同的字符,例如,radar toot madam. 分析:该问题的解决方案将使用 ...

  9. (Redis基础教程之五)如何在Redis中操作字符串

    如何在ubuntu18.04上安装和保护redis 如何连接到Redis数据库 如何管理Redis数据库和Keys 如何在Redis中管理副本和客户端 如何在Redis中管理字符串 如何在Redis中 ...

  10. 一条语句查看web日志排名前十的IP/URL页面及总数

    查看 Nginx web 访问日志访问量前十的ip 以及访问的网站页面地址 ,可以分析网站哪些页面受欢迎,以及访问量大的ip在干什么! 标签: <无> 代码片段(3)[全屏查看所有代码] ...