Spring Security教程系列(一)基础篇-2

 第 4 章 自定义登陆页面

 

Spring Security虽然默认提供了一个登陆页面,但是这个页面实在太简陋了,只有在快速演示时才有可能它做系统的登陆页面,实际开发时无论是从美观还是实用性角度考虑,我们都必须实现自定义的登录页面。

4.1. 实现自定义登陆页面

自己实现一个login.jsp,放在src/main/webapp/目录下。

 

4.2. 修改配置文件

在xml中的http标签中添加一个form-login标签。

 

<http auto-config="true">
<intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />1
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login login-page="/login.jsp" 2
authentication-failure-url
="/login.jsp?error=true" 3
default-target-url
="/index.jsp" />4 </http>

 

1

让没登陆的用户也可以访问login.jsp。[2]

这是因为配置文件中的“/**”配置,要求用户访问任意一个系统资源时,必须拥有ROLE_USER角色,/login.jsp也不例外,如果我们不为/login.jsp单独配置访问权限,会造成用户连登陆的权限都没有,这是不正确的。
 
2
 login-page表示用户登陆时显示我们自定义的login.jsp。

这时我们访问系统显示的登陆页面将是我们上面创建的login.jsp。
 
3
 authentication-failure-url表示用户登陆失败时,跳转到哪个页面。

当用户输入的登录名和密码不正确时,系统将再次跳转到/login.jsp,并添加一个error=true参数作为登陆失败的标示。
 
4
 default-target-url表示登陆成功时,跳转到哪个页面。[3]
 

4.3. 登陆页面中的参数配置

以下是我们创建的login.jsp页面的主要代码。

<body>

<div class="error  ${param.error == true ? '' : 'hide'}">
  ${sessionScope['SPRING_SECURITY_LAST_EXCEPTION'].message}
</div>
<form action="${pageContext.request.contextPath}/j_spring_security_check" method="post">
  <fieldset>
    <legend>登陆</legend>
    用户: <input type="text" name="j_username" style="width:150px;" value="${sessionScope['SPRING_SECURITY_LAST_USERNAME']}"/><br />
    密码: <input type="password" name="j_password" style="width:150px;" /><br />
    <input type="checkbox" name="_spring_security_remember_me" />两周之内不必登陆<br />
    <input type="submit" value="登陆"/>
    <input type="reset" value="重置"/>
  </fieldset>
</form>
</body>

 

1
 /j_spring_security_check,提交登陆信息的URL地址。

自定义form时,要把form的action设置为/j_spring_security_check。注意这里要使用绝对路径,避免登陆页面存放的页面可能带来的问题。[4]
 
2
 j_username,输入登陆名的参数名称。
 
3
 j_password,输入密码的参数名称
 
4
 _spring_security_remember_me,选择是否允许自动登录的参数名称。

可以直接把这个参数设置为一个checkbox,无需设置value,Spring Security会自行判断它是否被选中。
 
?????????? _spring_security_remember_me功能好像用不了,待续

 

4.4. 测试一下

经过以上配置,我们终于使用了一个自己创建的登陆页面替换了原来Spring Security默认提供的登录页面了。我们不仅仅是做个样子,而是实际配置了各个Spring Security所需的参数,真正将自定义登陆页面与Spring Security紧紧的整合在了一起。以下是使用自定义登陆页面实际运行时的截图。

 

 

登陆失败

 

登陆成功

 

[2] 有关匿名用户的知识,我们会在之后的章节中进行讲解。

[3] 登陆成功后跳转策略的知识,我们会在之后的章节中进行讲解。

[4] 关于绝对路径和相对路径的详细讨论,请参考http://www.family168.com/tutorial/jsp/html/jsp-ch-03.html#jsp-ch-03-04-01

 


 第 5 章 使用数据库管理资源

 

国内对权限系统的基本要求是将用户权限和被保护资源都放在数据库里进行管理,在这点上Spring Security并没有给出官方的解决方案,为此我们需要对Spring Security进行扩展。

5.1. 数据库表结构

这次我们使用五张表,user用户表,role角色表,resc资源表相互独立,它们通过各自之间的连接表实现多对多关系。

 

-- 资源
CREATE TABLE resc(
    id BIGINT,
    NAME VARCHAR(50),
    res_type VARCHAR(50),
    res_string VARCHAR(200),
    priority INTEGER,
    descn VARCHAR(200)
);
ALTER TABLE resc ADD CONSTRAINT pk_resc PRIMARY KEY(id);
ALTER TABLE resc CHANGE id id BIGINT  AUTO_INCREMENT;


-- 角色
CREATE TABLE role(
    id BIGINT,
    NAME VARCHAR(50),
    descn VARCHAR(200)
);
ALTER TABLE role ADD CONSTRAINT pk_role PRIMARY KEY(id);
ALTER TABLE role CHANGE id id BIGINT  AUTO_INCREMENT;

-- 用户
CREATE TABLE USER(
    id BIGINT,
    username VARCHAR(50),
    PASSWORD VARCHAR(50),
    STATUS INTEGER,
    descn VARCHAR(200)
);
ALTER TABLE USER ADD CONSTRAINT pk_user PRIMARY KEY(id);
ALTER TABLE USER CHANGE id id BIGINT  AUTO_INCREMENT;

-- 资源角色连接表
CREATE TABLE resc_role(
    resc_id BIGINT,
    role_id BIGINT
);
ALTER TABLE resc_role ADD CONSTRAINT pk_resc_role PRIMARY KEY(resc_id, role_id);
ALTER TABLE resc_role ADD CONSTRAINT fk_resc_role_resc FOREIGN KEY(resc_id) REFERENCES resc(id);
ALTER TABLE resc_role ADD CONSTRAINT fk_resc_role_role FOREIGN KEY(role_id) REFERENCES role(id);

-- 用户角色连接表
CREATE TABLE user_role(
    user_id BIGINT,
    role_id BIGINT
);
ALTER TABLE user_role ADD CONSTRAINT pk_user_role PRIMARY KEY(user_id, role_id);
ALTER TABLE user_role ADD CONSTRAINT fk_user_role_user FOREIGN KEY(user_id) REFERENCES USER(id);
ALTER TABLE user_role ADD CONSTRAINT fk_user_role_role FOREIGN KEY(role_id) REFERENCES role(id);
 
 
        
INSERT INTO USER(id,username,PASSWORD,STATUS,descn) VALUES(1,'admin','admin',1,'管理员');
INSERT INTO USER(id,username,PASSWORD,STATUS,descn) VALUES(2,'user','user',1,'用户');

INSERT INTO role(id,NAME,descn) VALUES(1,'ROLE_ADMIN','管理员角色');
INSERT INTO role(id,NAME,descn) VALUES(2,'ROLE_USER','用户角色');

INSERT INTO resc(id,NAME,res_type,res_string,priority,descn) VALUES(1,'','URL','/admin.jsp',1,'');
INSERT INTO resc(id,NAME,res_type,res_string,priority,descn) VALUES(2,'','URL','/**',2,'');

INSERT INTO resc_role(resc_id,role_id) VALUES(1,1);
INSERT INTO resc_role(resc_id,role_id) VALUES(2,1);
INSERT INTO resc_role(resc_id,role_id) VALUES(2,2);

INSERT INTO user_role(user_id,role_id) VALUES(1,1);
INSERT INTO user_role(user_id,role_id) VALUES(1,2);
INSERT INTO user_role(user_id,role_id) VALUES(2,2);

 

1.使用过滤器

  <custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/>

 

2.配置过滤器

<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">

       <beans:property name="authenticationManager"  ref="authenticationManager" />   
       <beans:property name="accessDecisionManager" ref="accessDecisionManager" /> 
       <beans:property name="securityMetadataSource" ref="databaseDefinitionSource" />
        
</beans:bean> 
      <beans:bean id="accessDecisionManager"
        class="org.springframework.security.access.vote.AffirmativeBased">
        <beans:property name="allowIfAllAbstainDecisions" value="false" />
        <beans:property name="decisionVoters">  
            <beans:list>  
                     <beans:ref bean="ftsp_security_urlVoter" />
            </beans:list>  
        </beans:property> 
    </beans:bean> 
    
         <beans:bean id="ftsp_security_urlVoter" class="springUrl.UrlVoter"/>
        <beans:bean id="databaseDefinitionSource" class="springUrl.FtspSecurityMetadataSource">

 

3.java类 FtspSecurityMetadataSource 实现FilterInvocationSecurityMetadataSource

实现方法getAttributes

package springUrl;

import java.util.ArrayList;
import java.util.Collection;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.access.SecurityConfig;
import org.springframework.security.web.FilterInvocation;
import org.springframework.security.web.access.intercept.FilterInvocationSecurityMetadataSource;

public class FtspSecurityMetadataSource implements
        FilterInvocationSecurityMetadataSource {

    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        String url = ((FilterInvocation) object).getRequestUrl();

        Collection<ConfigAttribute> confDef = new ArrayList<ConfigAttribute>();
        confDef.add(new SecurityConfig(url));
        return confDef;//1
    }

    public Collection<ConfigAttribute> getAllConfigAttributes() {
        // TODO Auto-generated method stub
        return null;
    }

    public boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(FilterInvocation.class);
    }



}
getAttributes中,可以对获取的url做一些处理,比如查出数据库的url和该url对比,改变该url。。。。等等

 

4.java类 UrlVoter实现AccessDecisionVoter<Object>

实现方法vote,attributes就是上文FtspSecurityMetadataSource类的getAttributes方法返回的参数,getAttributes返回了url的话,

这里可以查出数据库的url和该url对比,返回(ACCESS_GRANTED,ACCESS_ABSTAIN,ACCESS_DENIED),

ACCESS_GRANTED通过,

ACCESS_ABSTAIN,ACCESS_DENIED 禁止。

达到了数据库管理url权限

package springUrl;

import java.util.Collection;

import org.springframework.security.access.AccessDecisionVoter;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.FilterInvocation;




public class UrlVoter implements AccessDecisionVoter<Object> {


    public boolean supports(ConfigAttribute attribute) {
        return true;
    }


    public boolean supports(Class<?> clazz) {
        return clazz.isAssignableFrom(FilterInvocation.class);
    }


    public int vote(Authentication authentication, Object object, Collection<ConfigAttribute> attributes) {
        return ACCESS_GRANTED;
    }


}

如图

 

完整的applicationContext-security.xml

<beans:beans xmlns="http://www.springframework.org/schema/security"
  xmlns:beans="http://www.springframework.org/schema/beans"
  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.xsd">
     
     
<http auto-config="true" >
<access-denied-handler error-page="/accessDenied.jsp"/>
<intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?error=true" default-target-url="/index.jsp" />
  
  <!-- TODO 在SS3.0.x中,自定义的filter的配置要放在s:http里 -->
  <custom-filter ref="filterSecurityInterceptor" before="FILTER_SECURITY_INTERCEPTOR"/>
</http> 
         
<authentication-manager alias="authenticationManager">
    <authentication-provider>
<!--     <password-encoder hash="md5">
        <salt-source user-property="username"/>
    </password-encoder> -->
        <jdbc-user-service data-source-ref="dataSource" 
            users-by-username-query="select username,password,status as enabled
                                         from user
                                        where username=?"
            authorities-by-username-query="select u.username,r.name as authority
                                             from user u
                                             join user_role ur
                                               on u.id=ur.user_id
                                             join role r
                                               on r.id=ur.role_id
                                            where u.username=?"/>
    </authentication-provider>
</authentication-manager>
 
<beans:bean id="filterSecurityInterceptor" class="org.springframework.security.web.access.intercept.FilterSecurityInterceptor">

       <beans:property name="authenticationManager"  ref="authenticationManager" />   
       <beans:property name="accessDecisionManager" ref="accessDecisionManager" /> 
       <beans:property name="securityMetadataSource" ref="databaseDefinitionSource" />
        
</beans:bean> 
    
      <beans:bean id="accessDecisionManager"
        class="org.springframework.security.access.vote.AffirmativeBased">
        <beans:property name="allowIfAllAbstainDecisions" value="false" />
        <beans:property name="decisionVoters">  
            <beans:list>  
                     <beans:ref bean="ftsp_security_urlVoter" />
            </beans:list>  
        </beans:property> 
    </beans:bean> 
    
         <beans:bean id="ftsp_security_urlVoter" class="springUrl.UrlVoter"/>
        <beans:bean id="databaseDefinitionSource" class="springUrl.FtspSecurityMetadataSource">

</beans:bean> 

 
   
</beans:beans>

 

 

 


 

 

第 6 章 控制用户信息

让我们来研究一些与用户信息相关的功能,包括为用户密码加密,缓存用户信息,获得系统当前登陆的用户,获得登陆用户的所有权限。

6.1. MD5加密

任何一个正式的企业应用中,都不会在数据库中使用明文来保存密码的,我们在之前的章节中都是为了方便起见没有对数据库中的用户密码进行加密,这在实际应用中是极为幼稚的做法。可以想象一下,只要有人进入数据库就可以看到所有人的密码,这是一件多么恐怖的事情,为此我们至少要对密码进行加密,这样即使数据库被攻破,也可以保证用户密码的安全。

最常用的方法是使用MD5算法对密码进行摘要加密,这是一种单项加密手段,无法通过加密后的结果反推回原来的密码明文。

首先我们要把数据库中原来保存的密码使用MD5进行加密:

INSERT INTO USERS VALUES('admin','21232f297a57a5a743894a0e4a801fc3',TRUE)
INSERT INTO USERS VALUES('user','ee11cbb19052e40b07aac0ca060c23ee',TRUE)
 

现在密码部分已经面目全非了,即使有人攻破了数据库,拿到这种“乱码”也无法登陆系统窃取客户的信息。

下一步为了让Spring Security支持MD5加密,我们需要修改一下配置文件。

<authentication-provider>
    <password-encoder hash="md5"/>
    <jdbc-user-service data-source-ref="dataSource"/>
</authentication-provider>

上述代码中新增的黄色部分,将启用MD5算法。用户登录时,输入的密码是明文,需要使用password-encoder将明文转换成md5形式,然后再与数据库中的已加密密码进行比对。

这些配置对普通客户不会造成任何影响,他们只需要输入自己的密码,Spring Security会自动加以演算,将生成的结果与数据库中保存的信息进行比对,以此来判断用户是否可以登陆。

这样,我们只添加了一行配置,就为系统带来了密码加密的功能。

6.2. 盐值加密

实际上,上面的实例在现实使用中还存在着一个不小的问题。虽然md5算法是不可逆的,但是因为它对同一个字符串计算的结果是唯一的,所以一些人可能会使用“字典攻击”的方式来攻破md5加密的系统[5]。这虽然属于暴力解密,却十分有效,因为大多数系统的用户密码都不回很长。

实际上,大多数系统都是用admin作为默认的管理员登陆密码,所以,当我们在数据库中看到“21232f297a57a5a743894a0e4a801fc3”时,就可以意识到admin用户使用的密码了。因此,md5在处理这种常用字符串时,并不怎么奏效。

为了解决这个问题,我们可以使用盐值加密“salt-source”。

修改配置文件:

<authentication-provider>
    <password-encoder hash="md5">
        <salt-source user-property="username"/>
    </password-encoder>
    <jdbc-user-service data-source-ref="dataSource"/>
</authentication-provider>

 

在password-encoder下添加了salt-source,并且指定使用username作为盐值。

盐值的原理非常简单,就是先把密码和盐值指定的内容合并在一起,再使用md5对合并后的内容进行演算,这样一来,就算密码是一个很常见的字符串,再加上用户名,最后算出来的md5值就没那么容易猜出来了。因为攻击者不知道盐值的值,也很难反算出密码原文。

我们这里将每个用户的username作为盐值,最后数据库中的密码部分就变成了这样:

INSERT INTO USERS VALUES('admin','ceb4f32325eda6142bd65215f4c0f371',TRUE)
INSERT INTO USERS VALUES('user','47a733d60998c719cf3526ae7d106d13',TRUE)
  

 

6.3. 用户信息缓存

 

 

6.4. 获取当前用户信息

如果只是想从页面上显示当前登陆的用户名,可以直接使用Spring Security提供的taglib。

maven需要包

        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-taglibs</artifactId>
            <version>3.2.4.RELEASE</version>
        </dependency>

 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    <%String path=request.getContextPath(); %>
<%@ taglib uri="http://www.springframework.org/security/tags" prefix="sec" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>登录首页</title>
</head>
<body>
<div>username : <sec:authentication property="name"/></div>
<span color="red">登录成功!</span>
<a href="<%=path %>/welcome">welcome</a>
</body>
</html>

 

如果想在程序中获得当前登陆用户对应的对象。

UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()
    .getAuthentication()
    .getPrincipal();

如果想获得当前登陆用户所拥有的所有权限。

        Collection<? extends GrantedAuthority> authorities = userDetails.getAuthorities();

关于UserDetails是如何放到SecuirtyContext中去的,以及Spring Security所使用的TheadLocal模式,我们会在后面详细介绍。这里我们已经了解了如何获得当前登陆用户的信息。

 

[5] 所谓字典攻击,就是指将大量常用字符串使用md5加密,形成字典库,然后将一段由md5演算得到的未知字符串,在字典库中进行搜索,当发现匹配的结果时,就可以获得对应的加密前的字符串内容。

 

第 7 章 自定义访问拒绝页面

在我们的例子中,user用户是不能访问/admin.jsp页面的,当我们使用user用户登录系统之后,访问/admin.jsp时系统默认会返回403响应。

 

如果我们希望自定义访问拒绝页面,只需要随便创建一个jsp页面,让后将这个页面的位置放到配置文件中。

下面创建一个accessDenied.jsp

 

<%@ page contentType="text/html;charset=UTF-8"%>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Access Denied</title>
    <style type="text/css">
div.error {
    width: 260px;
    border: 2px solid red;
    background-color: yellow;
    text-align: center;
}
    </style>
  </head>
  <body>
    <h1>Access Denied</h1>
    <hr>
    <div class="error">
      访问被拒绝<br>
      ${requestScope['SPRING_SECURITY_403_EXCEPTION'].message}
    </div>
    <hr>
  </body>
</html>

下一步修改配置文件,添加自定义访问拒绝页面的地址。

 

<http auto-config="true"   access-denied-page="/accessDenied.jsp">

<intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?error=true" default-target-url="/index.jsp" />
</http> 

ide会报错,但不影响使用

 

查了一下

http://stackoverflow.com/questions/29786510/spring-security-warning-about-the-tag-access-denied-page-cvc-complex-type-3-2

应该就是spring security3和spring security2的区别吧

 

现在访问拒绝的页面就变成了下面这样:

 

 

查了官方文档,其实也可以这样写,效果一样又不警告

<http auto-config="true" >
<access-denied-handler error-page="/accessDenied.jsp"/>
<intercept-url pattern="/login.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
<intercept-url pattern="/**" access="ROLE_USER" />
<form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?error=true" default-target-url="/index.jsp" />
</http> 

 


 

 

第 8 章 动态管理资源结合自定义登录页面

 

 


 

第 9 章 中文用户名

 


 

 

第 10 章 判断用户是否登录

<http auto-config='true'>
    <intercept-url pattern="/admin.jsp" access="ROLE_ADMIN" />
    <intercept-url pattern="/**" access="IS_AUTHENTICATED_FULLY" />
</http>

这样除了/admin.jsp之外所有的URL,只要是登录用户就可以访问了。

 

 

源码:http://pan.baidu.com/s/1pKnxLRP

 

posted @ 2015-11-27 11:13  crazyYong  阅读(1064)  评论(0编辑  收藏  举报