原文链接:http://sarin.iteye.com/blog/829738
现在来说Security部分。Spring Security框架是Acegi Security的升级,这个框架就是利用了多重过滤的机制对请求进行处理,将符合要求的请求放行,不符合要求的请求阻止下来,这是最大的原理。下面先来看看简单的url过滤吧。
先写一个用于验证身份的登录页面:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
|
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<
html
>
<
head
>
<
title
>Login</
title
>
</
head
>
<
body
>
<
c:if
test
=
"${not empty param.error}"
>
<
font
color
=
"red"
>Login error.<
br
/>
</
font
>
Reason:${sessionScope["SPRING_SECURITY_LAST_EXCEPTION"].message}
</
c:if
>
<
form
method
=
"POST"
action
=
"<c:url value="
/login"/>">
<
table
>
<
tr
>
<
td
align
=
"right"
>Username</
td
>
<
td
><
input
type
=
"text"
name
=
"j_username"
/></
td
>
</
tr
>
<
tr
>
<
td
align
=
"right"
>Password</
td
>
<
td
><
input
type
=
"password"
name
=
"j_password"
/></
td
>
</
tr
>
<
tr
>
<
td
colspan
=
"2"
align
=
"right"
><
input
type
=
"submit"
value
=
"Login"
/>
<
input
type
=
"reset"
value
=
"Reset"
/></
td
>
</
tr
>
</
table
>
</
form
>
</
body
>
</
html
>
|
做一些说明,使用Spring Security时,默认的登录验证地址是j_spring_security_check,验证的用户名是j_username,密码是j_password,对于用户名和密码我们不需要修改,使用其默认值即可,而验证路径通常我们想使用自定义地址,这就需要在security中进行配置,后面会看到,这里还会看到如果验证失败,会把失败信息打印出来,就是JSTL的c:if段的作用。下面来看最基本的Security框架作用,拦截URL请求。在board-security.xml配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
<?
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-3.0.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.0.xsd">
<
security:http
auto-config
=
"true"
>
<
security:intercept-url
pattern
=
"/messageList.htm"
access
=
"ROLE_ADMIN,ROLE_USER,ROLE_GUEST"
/>
<
security:intercept-url
pattern
=
"/messagePost.htm"
access
=
"ROLE_ADMIN,ROLE_USER"
/>
<
security:intercept-url
pattern
=
"/messageDelete.htm"
access
=
"ROLE_ADMIN"
/>
<
security:anonymous
username
=
"guest"
granted-authority
=
"ROLE_GUEST"
/>
</
security:http
>
<
security:authentication-manager
>
<
security:authentication-provider
>
<
security:user-service
>
<
security:user
name
=
"admin"
authorities
=
"ROLE_ADMIN,ROLE_USER"
password
=
"secret"
/>
<
security:user
name
=
"user1"
authorities
=
"ROLE_USER"
password
=
"1111"
/>
</
security:user-service
>
</
security:authentication-provider
>
</
security:authentication-manager
>
</
beans
>
|
配置文件中首先是对http请求的拦截,这里使用了自动配置auto-config,那么当请求到达时,Security框架会让我们进行身份验证,我们拦截的url模式已经在其中配置出来,三个请求分别对应不同的权限,而且messageList.htm还开放了匿名访问功能,要提供匿名访问,就要配置<anonymous>,这里我们配置匿名用户名为guest,角色是ROLE_GUEST,这里的角色都是ROLE_开头,是Spring Security框架默认使用的,我们不用去更改,这也很清楚。首先我们启动应用,来访问唯一的匿名功能,之后我们看到如下效果:
可以看到,现在的角色是ROLE_GUEST,那么就直接看到,没有验证身份,若我们要发布消息呢,点击Post链接,看看效果:
要求身份验证了,这就说明对url的拦截起作用了,想发布消息,权限不够了,要验证身份了,注意这里这个页面并不是我们前面写的那个页面,而是Security框架的默认验证页面,为什么没有使用我们所写的页面呢?因为我们还没有配置它啊,当然不会被识别到了。我们来看看默认的页面源码是什么样子的:
1
2
3
4
5
6
7
8
9
|
<
html
><
head
><
title
>Login Page</
title
></
head
><
body
onload
=
'document.f.j_username.focus();'
>
<
h3
>Login with Username and Password</
h3
><
form
name
=
'f'
action
=
'/j_spring_security_check'
method
=
'POST'
>
<
table
>
<
tr
><
td
>User:</
td
><
td
><
input
type
=
'text'
name
=
'j_username'
value
=
''
></
td
></
tr
>
<
tr
><
td
>Password:</
td
><
td
><
input
type
=
'password'
name
=
'j_password'
/></
td
></
tr
>
<
tr
><
td
colspan
=
'2'
><
input
name
=
"submit"
type
=
"submit"
/></
td
></
tr
>
<
tr
><
td
colspan
=
'2'
><
input
name
=
"reset"
type
=
"reset"
/></
td
></
tr
>
</
table
>
</
form
></
body
></
html
>
|
可以看到这里的默认请求路径就是/j_spring_security_check了,不过这里我们已经可以使用我们配置的用户来登录了,之前在配置文件中的admin和user1,它们拥有的权限不同,那么我们使用user1登录,来发布消息。验证通过,出现消息输入页面:
下面发布消息,之后能看到消息的列表了,这对ROLE_USER的角色都是可以查看的。
没有把Author的信息打印出来,为什么?我们在这里对这个自动进行了限制,来看一下页面是怎么写的:
1
2
3
4
5
6
|
<
security:authorize
ifAllGranted
=
"ROLE_ADMIN,ROLE_USER"
>
<
tr
>
<
td
>Author</
td
>
<
td
>${message.author}</
td
>
</
tr
>
</
security:authorize
>
|
这里说的是拥有ROLE_ADMIN和ROLE_USER两种角色才能显示author信息,显然我们权限不够了,当然把这里修改为ifAnyGranted=”ROLE_ADMIN,ROLE_USER”就可以显示出来了,All和Any的区别嘛,很容易理解。还有一个属性是ifNotGranted,不用说你也会明白它是什么意思了,我们现在修改为ifAnyGranted=”ROLE_ADMIN,ROLE_USER”,刷新页面,就会看到如下内容了:
其实这已经是在扩展Security框架的视图功能了,就是这么使用的,如果想了解security框架标签库其它标签,那么去参考官方文档吧,因为你已经知道该怎么去套用了。
该试试删除功能了,当前用户角色是ROLE_USER,想删除肯定是不可以的了,那么会是怎么样的效果呢,点击Delete链接,看一下吧:
非常不幸,被拦截下来了,HTTP 403表示没有权限,那么就对了,Security框架起作用了,这就是我们想要的效果了。
Security框架的基本URL拦截到此就说完了,是不是很简单?下面就来定制一些操作吧,我们既然编写了自定义登录页面,得用上吧,还有Logout退出功能没用呢。下面就对这基本的配置进行第一次扩展,我们这样做:
1
2
3
4
5
6
7
8
9
10
11
12
|
<
security:http
>
<
security:intercept-url
pattern
=
"/messageList.htm"
access
=
"ROLE_ADMIN,ROLE_USER,ROLE_GUEST"
/>
<
security:intercept-url
pattern
=
"/messagePost.htm"
access
=
"ROLE_ADMIN,ROLE_USER"
/>
<
security:intercept-url
pattern
=
"/messageDelete.htm"
access
=
"ROLE_ADMIN"
/>
<
security:form-login
login-page
=
"/login.jsp"
login-processing-url
=
"/login"
default-target-url
=
"/messageList.htm"
authentication-failure-url
=
"/login.jsp?error=true"
/>
<
security:logout
logout-success-url
=
"/login.jsp"
/>
</
security:http
>
|
首先去掉auto-config,因为要定制,不让Security按它默认的执行。那么登录验证就配置吧,login-page属性配置的是登录页面,就是我们前面所写的,login-processing-url就是我们处理登录逻辑的请求地址,默认的是j_spring_security_check,前面也说过了,default_target_url就是默认的登录成功转向的目标地址,这里是消息列表页面。最后一个属性是authentication-failure-url,很明白了,就是验证失败转向的页面,这里我们附加一个参数error,页面里面也有体现,就是用它来控制失败信息的打印的。下面一个是配置退出,logout-success-url就是退出后转向的页面,这里是到登录页面,没错,退出后回到登录页面。下面来看看效果吧,修改完毕重启Jetty:
由于去掉了匿名访问,那么直接请求messageList.htm就会为我们跳转到登录页面了,进行身份验证,此时我们输入一个错误的信息,看看能捕捉到什么:
验证失败错误会出现Bad credentials,这里不判断是用户不存在还是密码错误,统一是登录凭据错误。输入正确的信息就可以重复上述操作了。使用admin登录成功,会出现:
至此基本的Security拦截操作已经说完了,是不是很简单呢。当然这是测试的,真实应用中我们的用户不可能这么配置,因为都是存放在数据库中的,那么Security能不能支持数据库用户验证呢?答案是肯定的。只是需要一些扩展配置,这里Security整合了一些数据库验证的操作,要符合Security的验证模式,那么要么我们重新设计数据库,要么在原有基础之上来修改一下数据库设计。这里我们先看一下Security框架默认支持的数据库设计吧,就是它默认SQL查询语句所支持的内容。
这两个表明是默认的,这么写Security会自己识别出来,不用我们书写SQL语句了。先来看看表设计吧,就这些信息就够Security进行验证了。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
CREATE
TABLE
`users` (
`USERNAME`
varchar
(10)
NOT
NULL
,
`
PASSWORD
`
varchar
(32)
NOT
NULL
,
`ENABLED` tinyint(1)
NOT
NULL
,
PRIMARY
KEY
(`USERNAME`)
) ENGINE=InnoDB
DEFAULT
CHARSET=utf8
CREATE
TABLE
`authorities` (
`USERNAME`
varchar
(10)
NOT
NULL
,
`AUTHORITY`
varchar
(10)
NOT
NULL
,
KEY
`FK_USERNAME_AUTHORITY` (`USERNAME`),
CONSTRAINT
`FK_USERNAME_AUTHORITY`
FOREIGN
KEY
(`USERNAME`)
REFERENCES
`users` (`USERNAME`)
ON
DELETE
NO
ACTION
ON
UPDATE
NO
ACTION
) ENGINE=InnoDB
DEFAULT
CHARSET=utf8
|
两个表之间有一个外键的关联,是用户名关联,而且我们还进行了md5密码扩展,这也要在Security框架进行配置,在表中插入一些信息,就可以进行数据库验证了,此时Security框架的配置如下,修改认证管理器:
1
2
3
4
5
6
|
<
security:authentication-manager
>
<
security:authentication-provider
>
<
security:password-encoder
ref
=
"md5Encoder"
/>
<
security:jdbc-user-service
data-source-ref
=
"dataSource"
/>
</
security:authentication-provider
>
</
security:authentication-manager
>
|
这里我们配置了jdbc数据源和密码编码器,因为连MD5加密方式也是我们自定义的,这样安全系数更高。要使用自定义的加密器,别忘了编写加密器的bean。
1
|
<
bean
id
=
"md5Encoder"
class
=
"org.ourpioneer.board.util.MD5Encoder"
/>
|
加密器类需要实现PasswordEncoder接口,然后编写我们自己的加密方案,加密器很简单,如下设计:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
package org.ourpioneer.board.util;
import org.springframework.dao.DataAccessException;
import org.springframework.security.authentication.encoding.PasswordEncoder;
public class MD5Encoder implements PasswordEncoder {
public String encodePassword(String origPwd, Object salt)
throws DataAccessException {
return MD5.getMD5ofStr(origPwd);
}
public boolean isPasswordValid(String encPwd, String origPwd, Object salt)
throws DataAccessException {
return encPwd.equals(encodePassword(origPwd, salt));
}
}
|
其中使用到的MD5加密类为:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
|
package
org.ourpioneer.board.util;
import
java.security.MessageDigest;
/**
* 标准MD5加密方法,使用java类库的security包的MessageDigest类处理 <BR>
* 也可变为非标准MD5,请修改下面的移位算法
*
* @author Nanlei
*
*/
public
class
MD5 {
/**
* 获得MD5加密密码的方法
*/
public
static
String getMD5ofStr(String origString) {
String origMD5 =
null
;
try
{
MessageDigest md5 = MessageDigest.getInstance(
"MD5"
);
byte
[] result = md5.digest(origString.getBytes());
origMD5 = byteArray2HexStr(result);
}
catch
(Exception e) {
e.printStackTrace();
}
return
origMD5;
}
/**
* 处理字节数组得到MD5密码的方法
*/
private
static
String byteArray2HexStr(
byte
[] bs) {
StringBuffer sb =
new
StringBuffer();
for
(
byte
b : bs) {
sb.append(byte2HexStr(b));
}
return
sb.toString();
}
/**
* 字节标准移位转十六进制方法
*/
private
static
String byte2HexStr(
byte
b) {
String hexStr =
null
;
int
n = b;
if
(n <
0
) {
// 若需要自定义加密,请修改这个移位算法即可
n = b &
0x7F
+
128
;
}
hexStr = Integer.toHexString(n /
16
) + Integer.toHexString(n %
16
);
return
hexStr.toUpperCase();
}
/**
* 提供一个MD5多次加密方法
*/
public
static
String getMD5ofStr(String origString,
int
times) {
String md5 = getMD5ofStr(origString);
for
(
int
i =
0
; i < times -
1
; i++) {
md5 = getMD5ofStr(md5);
}
return
getMD5ofStr(md5);
}
/**
* 密码验证方法
*/
public
static
boolean
verifyPassword(String inputStr, String MD5Code) {
return
getMD5ofStr(inputStr).equals(MD5Code);
}
/**
* 多次加密时的密码验证方法
*/
public
static
boolean
verifyPassword(String inputStr, String MD5Code,
int
times) {
return
getMD5ofStr(inputStr, times).equals(MD5Code);
}
/**
* 提供一个测试的主函数
*/
public
static
void
main(String[] args) {
System.out.println(
"123:"
+ getMD5ofStr(
"123"
));
System.out.println(
"123456789:"
+ getMD5ofStr(
"123456789"
));
System.out.println(
"pioneer:"
+ getMD5ofStr(
"pioneer"
));
System.out.println(
"123:"
+ getMD5ofStr(
"123"
,
4
));
}
}
|
加密工作已经准备好,之前配置的数据源是:
1
2
3
4
5
6
7
8
|
<
bean
id
=
"dataSource"
class
=
"org.springframework.jdbc.datasource.DriverManagerDataSource"
>
<
property
name
=
"driverClassName"
value
=
"com.mysql.jdbc.Driver"
/>
<
property
name
=
"url"
value
=
"jdbc:mysql://localhost:3306/board"
/>
<
property
name
=
"username"
value
=
"root"
/>
<
property
name
=
"password"
value
=
"123"
/>
</
bean
>
|
别忘了加入JDBC的驱动程序,之后我们就可以使用数据库用户验证了,剩下的步骤就和前面是一样的了。