山哥做的网站本来已经有登陆控制,用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);
Issue 2, 用了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 3, 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());
}
}
待续……
网友评论