Spring Security 和 Spring Cache 填坑记录

山哥做的网站本来已经有登陆控制,用JDBC来存SessionId相关的信息。但是权限控制的设计在扩展性方面不太好。于是想试试高大上的 Spring Security ,听说Apache Shino也不错而且简单很多,但是Spring 的Actuator要用Spring Security,只好硬着头皮啃这件难啃的货了。用过之后,发现这货真的像网上人民吐槽的那样,相当的复杂麻烦!有好几次想放弃不用它了,但是凭着不服输的精神,还是钻研了下去。花了一个星期的工余时间,总算把原来的解决方案迁移到了Spring Security上。过程之中填坑无数,结果却是喜人的!于是纪录一下一些重要的point。

至于Spring Cache,上了Spring Security 之后,如果不上Cache,那性能我相信是惨不忍暏的!为什么?但凡登陆验证,你得和数据库里的username和password来compare吧?它的jdbc模式虽然自带Cache,但我不想填更多的坑,于是自己实现一个 UserDetailsService, 这样我的数据库结构自由很多,甚至以后不用关系型 数据库也可以。用了debug模式,发现访问每个页面它都会调用一次UserDetailsService,我不想测试它每次去读数据库的性能,人生苦短啊大叔!上缓存吧!!

Issue 1, 这个问题是 Spring Cache 贡献出来的。如果你也用同样的写法,可能你在开发环境没问题,但生产环境一定会遇到。

出错信息:java.lang.IllegalArgumentException: Null key returned for cache operation (maybe you are using named params on classes without debug info?)
根本原因:以下代码里,自定义了Key,于是Key generator不起效,它会用SpEL来把 name 这个参数作为Key。这是官网教程的例子,为什么会行不通呢?再反复斟酌官网教程,终于明白它说什么了,就是如果编译没选debug模式,编译出来的class文件是没有参数名的信息的,那么反射机制来获取这个参数的值,就找不到参数名字!只能用 #a0或者#p0来指代第一个参数,依此类推。(If for some reason the names are not available (e.g. no debug information), the argument names are also available under the #a<#arg> where #arg stands for the argument index (starting from 0).)
会出错的写法,引用 参数 name作为Cache的key。

@Cacheable(cachnames="user", key="#name")
public User findByName(String name);

正确的万能写法,这里把 user_ 作为 key的前缀,传参 name作为后缀。大功告成!亲个嘴儿!

@Cacheable(cachnames="user", key="'user_'.concat(#a0)")
public User findByName(String name);

参考网址:http://docs.spring.io/spring/docs/current/spring-framework-reference/html/cache.html#cache-spel-context

Issue 2,如果你的Entity类作为Cache, 用了JCache,那么一定要实现Serializable,里面所有的集合类,所有的自定义类,都要是Serializable。(比如ArrayList, LinkedList, ConcurrentSet等等都可以)

如果你做了Join Table,那么不能用LAZY的方式Join,否则从缓存解出来就会有问题。一定要用EAGER。
Join Table一定要注意,你的类返回不可以是Null纪录,否则从缓存解出来无法还原成一个真实的Entity,又会出错…

Issue 3, 用了Spring Security之后,页面ajax里的POST方法失效!Chrome环境下看,返回代码是 403 Forbidden。

出错原因: csrf 是默认启用的,如果是thymeleaf作为渲染器,在html form里面,spring security会自动加入一个hidden的 _csrf.token,可是 如果你上了 ajax,那臣妾就表示无能为力了。。
解决方案:请看如下两步骤,用来解决jquery的ajax request for POST:
Step 1, 在<head>里加如下function

<script type="text/javascript">
 function sendCsrfHeader (request) {
   var token = '[[${_csrf.token}]]';
   var header = '[[${_csrf.headerName}]]';
   request.setRequestHeader(header, token);
 }
</script>

Step 2, 在每个 POST的ajax里面,加上beforeSend:

$.ajax({
   type : "POST",
   url : [[@{/user/changePassword}]],
   dataType : 'json',
   async : true,
   data : { },
   beforeSend: function(request) {
     sendCsrfHeader(request);
   },
   success : function(responseText) {
  ...

Issue 4, Spring Security 的 authentication.isAuthenticated()不好使!明明没登陆,为什么这个为true呢?

出错原因: 虽然你没登陆,但是 anonymousUser 登陆了,这个时候你匿名能访问的页面里,也是 authenticated的!
解决方案:在thymeleaf里用spring4 dialect
1, Maven添加以下依赖:注意 3.0.2,RELEASE的 springsecurity4不支持3.0.2的thymeleaf,所以要指定version 为3.0.1.RELEASE.

<properties>
  <thymeleaf.version>3.0.2.RELEASE</thymeleaf.version>
  <thymeleaf-layout-dialect.version>2.1.1</thymeleaf-layout-dialect.version>
</properties>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
  <groupId>org.thymeleaf.extras</groupId>
  <artifactId>thymeleaf-extras-springsecurity4</artifactId>
  <version>3.0.1.RELEASE</version>
</dependency>

2, 在页面中用以下标签:
<div th:if="${#authentication.name == 'anonymousUser'}">请登陆</div>
这个办法太挫,而且现在Spring Security 它 hardcode了anonymousUser,以后指不定用什么名字,于是山哥写了一个工具类,拒绝hardcode!:

@Service
public class LoginUtil {
   //Use this to get the Anonymous User name. Because do not want to hardcode.
   private static AnonymousAuthenticationFilter anonymousAuthenticationFilter = new AnonymousAuthenticationFilter("internal_use");
   public boolean isLogin(Authentication aut) {
     if(aut == null || !aut.isAuthenticated()) {
       return false;
     } else
       return !aut.getName().equals(anonymousAuthenticationFilter.getPrincipal());
   }
}

待续……

推荐阅读更多精彩内容