Spring Security 4入门

2022-11-15 13:33:35 浏览数 (1)

1 Spring Security简介

        Spring Security是Spring框架中的独立项目,是一个安全框架,能够为基于Spring的Java EE应用提供声明式的安全访问控制解决方案。它提供了一组可以在Spring应用上下文中配置的安全对象,充分利用了Spring DI(依赖注入)和AOP(面向切面)功能,为应用系统提供声明式的访问控制机制,以减少了企业级开发中需要重复编写大量安全代码的工作。

2 Spring Security 4使用入门

本文基于Spring Security的“4.0.3.RELEASE”版本讲述,应配合Spring主框架“4.2.5. RELEASE”版本使用,具体配置有别于早期的Spring 3.x。

2.1 简单启用Security

下面以Spring MVC项目为例,使用Security框架实现访问控制。

(1)导入依赖

Spring Security是独立于Spring的安全框架,我们可以通过Maven,导入相关的依赖

代码语言:javascript复制
               <!-- Spring Security 4 -->

                 <dependency>

                         <groupId>org.springframework.security</groupId>

                         <artifactId>spring-security-web</artifactId>

                         <version>4.0.3.RELEASE</version>

                 </dependency>

                 <dependency>

                         <groupId>org.springframework.security</groupId>

                         <artifactId>spring-security-config</artifactId>

                         <version>4.0.3.RELEASE</version>

                 </dependency>

                 <!-- Spring Security 的JSP标签,页面用到Security标签才需要导入 -->

                 <dependency>

                         <groupId>org.springframework.security</groupId>

                         <artifactId>spring-security-taglibs</artifactId>

                         <version>4.0.3.RELEASE</version>

                 </dependency>

(2)在web.xml中添加过滤器,启动Spring Security功能

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>

<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee"

        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xsi:schemaLocation="http://java.sun.com/xml/ns/javaee

        http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

    ......

        <!-- 启动Spring容器的监听器 -->

        <listener>

        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

        </listener>

        <context-param>

                 <param-name>contextConfigLocation</param-name>

                 <param-value>classpath:spring-security.xml</param-value>

        </context-param>

    ......

        <!-- Spring Security -->

        <filter>

                 <filter-name>springSecurityFilterChain</filter-name>

                 <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>

        </filter>

        <filter-mapping>

                 <filter-name>springSecurityFilterChain</filter-name>

                 <url-pattern>/*</url-pattern>

        </filter-mapping>

    ......

</web-app>

        在上述配置中:

  1. 监听器“ContextLoaderListener”用于在web项目启动时创建Spring容器,只要使用了Spring的web项目一般都需要配置。值得注意的是其监听器参数“contextConfigLocation”中指定了一个Spring Security的配置文件“classpath:spring-security.xml”,启动Spring容器的同时也加载了Security的配置。
  2. 过滤器“DelegatingFilterProxy”用于启用Spring Security对web请求的拦截和检验功能。

(3)编写spring-security.xml配置文件,为Security提供配置。

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>

<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.xsd

           http://www.springframework.org/schema/security

           http://www.springframework.org/schema/security/spring-security.xsd">

    <!-- 设置无需安全验证的路径模式 -->

    <http pattern="/content/**" security="none" />

    <!-- 启用SpEL设置安全模式 -->

        <http auto-config="true" use-expressions="true">

                 <!-- 只允许拥有“ADMIN”权限的用户访问 -->

               <intercept-url pattern="/admin/**" access="hasAuthority('管理员')"/>

                 <!-- 只允许登录用户访问 -->

               <intercept-url pattern="/account/modify" access="isAuthenticated()"/>

        </http>

        <!-- 预定义一些账户 -->

        <authentication-manager>

                 <authentication-provider>

                         <user-service>

                                  <user name="zhang3" password="123" authorities="管理员"/>

                                <user name="li4" password="123" authorities="普通用户"/>

                         </user-service>

                 </authentication-provider>

        </authentication-manager>

</beans:beans>

hasAuthority()是Spring Security 所支持的SpEL表达式,表示“拥有”哪些权限。相关的SpEL还有如下这些。

2.2 指定deny页面

当已登录用户没有访问某些页面的权限时,Spring Security默认会返回Http的403错误。

默认的权限报错界面

        如果希望看到自定义的友好界面,则需要在<http>设置<access-denied-handler>

代码语言:javascript复制
<http auto-config="true" use-expressions="true">

                 <!-- 指定没有权限时显示的页面 -->

                 <access-denied-handler error-page="/deny" />

                 ……

 </http>

配置后的授权报错页面

2.3 自定义登录页面

        Spring Security默认提供了登录界面和验证功能,但的登录页面比较简陋,基本不符合我们的需求。幸好,我们可以通过配置的方式快速将其替代为自己的登录页面,只要确保其中的提交路径信息正确就可以了。

默认的登录页面

(1)通过<http>配置中的<form-login>元素,可以替换登录页面的相关信息。

代码语言:javascript复制
<http auto-config="true" use-expressions="true">

                 <!-- 指定登录页面路径、登录处理路径 -->

                 <form-login

                         default-target-url="/index"

                         login-page="/login"

                         authentication-failure-url="/login?error=1"

                         login-processing-url="/checklogin" />

                 ……

 </http>

        其中各配置属性的含义如下:

        default-target-url                      指定登录成功后的默认跳转位置;

        login-page                                 指定登录页面的URL

        authentication-failure-url        指定登录验证失败的跳转路径error参数用于标识报错

login-processing-url                 指定登录验证功能的提交路径,该功能无需编码

(2)登录页面的实现

代码语言:javascript复制
       <form action="${pageContext.request.contextPath}/checklogin" method="post">

    <fieldset id="editForm">

        <legend>用户登录</legend>

        <div>

            <label class="fieldname">用户名</label>

            <input type="text" name="username" />

        </div>

        <div>

            <label class="fieldname">密码</label>

            <input type="password" name="password" />

        </div>

        <div>

            <label class="fieldname">&nbsp;</label>

            <input type="submit" value="登录" />

        </div>

        <c:if test="${param.error!=null}">

        <p style="color:red;text-align: center;line-height: 1;">用户名或密码有误。</p>

        </c:if>

        <c:if test="${param.logout != null}">

                 <p style="color:red;text-align: center;line-height: 1;">登录已注销。</p>

                 </c:if>

        <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/>

    </fieldset>

    </form>

        在上述页面中:

  1. 登录页面中用户名字段必须为名为“username”,密码字段必须为“password”。
  2. error参数和logout参数是可选的,用来标识登录失败或已经注销。
  3. “<input type="hidden" name="
代码语言:javascript复制
<http auto-config="true" use-expressions="true">

<csrf />

……

</http>

2.4 启用方法级别拦截

        对于MVC框架而言,URL请求往往是被映射到Controller类的Action方法上去的,因此就URL进行安全拦截似乎不是最合适的方式。Spring Security不仅仅能提供基于URL路径的HTTP拦截,还可以提供基于方法级别的调用拦截;因此,我们可以把权限控制设置到对应的Controller类或Action方法上去,让权限配置更为灵活。

(1)启用方法级别的权限拦截

        需要使用方法级别的拦截,需要设置“<security:global-method-security pre-post-annotations="enabled" />”。

        在Spring MVC项目中,我们可以修改MVC配置文件“springmvc-servlet.xml”,添加

代码语言:javascript复制
<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

        xmlns:context="http://www.springframework.org/schema/context"

        xmlns:mvc="http://www.springframework.org/schema/mvc"

        xmlns:security="http://www.springframework.org/schema/security"

        xsi:schemaLocation="http://www.springframework.org/schema/mvc

                                  http://www.springframework.org/schema/mvc/spring-mvc.xsd

                                  http://www.springframework.org/schema/beans

                                  http://www.springframework.org/schema/beans/spring-beans.xsd

                                  http://www.springframework.org/schema/context

                      http://www.springframework.org/schema/context/spring-context.xsd

                      http://www.springframework.org/schema/security

                      http://www.springframework.org/schema/security/spring-security.xsd">

                     

    <!-- 开启Spring Security 方法级注解配置 -->

    <security:global-method-security pre-post-annotations="enabled" />

    ....

</beans>

(2)使用 @PreAuthorize 注解

        @PreAuthorize注解可以应用在类或方法上方,标识该类或该方法执行前需要提供相应的权限。

        将权限设置到Action方法上:

代码语言:javascript复制
@Controller

@RequestMapping("/account")

public class AccountController {

        ……

        @PreAuthorize("isAuthenticated()")

        @RequestMapping("/modify")

        public String modify(){

                 return "account-modify";

        }

}

        将权限设置到Controller上,则为此控制器的所有Action方法设置权限:

代码语言:javascript复制
@Controller

@RequestMapping("/account")

public class AccountController {

        ……

        @PreAuthorize("isAuthenticated()")

        @RequestMapping("/modify")

        public String modify(){

                 return "account-modify";

        }

}

2.5 自定验证和授权过程

        上述示例中使用的用户账号及权限,都是直接配置在配置文件中的,实际开发中,用户账号和权限应该是存放在数据库中的。Spring Security提供了多种方式可以自定义账户和权限信息的来源,其中最简单的方式是重写“UserDetailsService”接口中的“loadUserByUsername”方法,根据用户名编码查询并返回用户账户信息。Spring Security中使用UserDetails接口描述用户信息。

(1)数据库结构

用户表Users

角色标Role

用户-角色 多对多中间表 UserRole

(2)用户信息类(UserDetails接口)的实现如下

代码语言:javascript复制
public class UserDetailsImpl implements UserDetails {

        ……

        private Collection<GrantedAuthority> authorities;

        private String password;

        private String username;

        private boolean accountNonExpired;

        private boolean accountNonLocked;

        private boolean credentialsNonExpired;

        private boolean enable;

        public Collection<GrantedAuthority> getAuthorities() {

                 return authorities;

        }

        public String getPassword() {

                 return password;

        }

        public String getUsername() {

                 return username;

        }

        public boolean isAccountNonExpired() {

                 return accountNonExpired;

        }

        public boolean isAccountNonLocked() {

                 return accountNonLocked;

        }

        public boolean isCredentialsNonExpired() {

                 return credentialsNonExpired;

        }

        public boolean isEnabled() {

                 return enable;

        }

}

(3)用户信息的查询类(UserDetailsService接口)的实现如下。其中调用了数据访问类UserDao查询用户信息和权限。

代码语言:javascript复制
@Service("userDetailsService")

public class UserDetailsServiceImpl implements UserDetailsService {

        @Autowired

        private UserDao userDao;

        //关键接口方法

        public UserDetails loadUserByUsername(String username)

                         throws UsernameNotFoundException {

                 User user = userDao.fetchByUsername(username);

                 if(user==null)

                         throw new UsernameNotFoundException("用户名有误。"); //该异常标识失败

                 UserDetailsImpl details = new UserDetailsImpl(

                                  getAuthoritiesByUserId(user.getId()),      //authorities Set<GrantedAuthority>

                                  user.getPassword(),

                                  user.getUsername(),

                                  true, //accountNonExpired

                                  true, //accountNonLocked

                                  true, //credentialsNonExpired

                                  true  //enable

                         );

                 return details;

        }

        //根据用户名(用户Id)获取用户权限

        private Set<GrantedAuthority> getAuthoritiesByUserId(int userId){

                 Set<GrantedAuthority> set = new HashSet<GrantedAuthority>();

                 List<Role> roles = userDao.getRolesByUserId(userId);

                 for(Role r : roles){

                         set.add(new SimpleGrantedAuthority(r.getName()));

                 }

                 return set;

        }

}

(4)修改Spring Security配置文件“spring-security.xml”,更换用户账号的来源。

代码语言:javascript复制
        <!-- 管理登录账号 -->

        <authentication-manager>

                 <authentication-provider user-service-ref="userDetailsService" />

        </authentication-manager>

2.6 关于注销

        Spring Security还默认提供了注销登录功能(logout),只需在<http>元素下配置<logout>子元素,就能实现注销登录。

代码语言:javascript复制
<!-- 指定注销路径 -->

                 <logout

                         logout-url="/logout"

                         logout-success-url="/login?logout=1"

                         invalidate-session="true" />

        上述配置中:

        logout-url                指定了注销请求的提交地址

logout-success-url 指定了注销成功后跳转的页面,其中的“logout=1”仅为表示,可选

        值得注意的是,如果启用了防范csrf的令牌功能,则必须使用POST方式并且提供防csrf令牌才成功请求logout功能,否则会出现404访问错误,而默认防csrf令牌功能是开启的!

2.7 获取已登录用户信息

(1)使用Spring Security标签获取用户信息

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

代码语言:javascript复制
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>

<div>username : <sec:authentication property="name"/></div>

        使用该标签库,则需要导入以下Maven依赖。

代码语言:javascript复制
<!-- Spring Security 的JSP标签,页面用到Security标签才需要导入 -->

                 <dependency>

                         <groupId>org.springframework.security</groupId>

                         <artifactId>spring-security-taglibs</artifactId>

                         <version>4.0.3.RELEASE</version>

                 </dependency>

(2)在Controller中获取用户信息

如果想在程序中获得当前登陆用户对应的对象,可以使用以下代码

代码语言:javascript复制
UserDetails userDetails = (UserDetails) SecurityContextHolder.getContext()

.getAuthentication().getPrincipal();

如果想获得当前登陆用户所拥有的所有权限,可以使用以下代码。

代码语言:javascript复制
GrantedAuthority[] authorities = userDetails.getAuthorities();

        必要时,我们还可以继承并扩展GrantedAuthority和UserDetails类,进一步保存自定义的用户信息。

3 记住我的登录状态

        很多时候,用户希望记住自己的登录状态,以便下次免登录。Spring Security内置了remember-me的Cookie机制,只要配置就可实现记住登录状态的功能。

(1)在<http>元素下添加<remember-me>子元素,开启功能。

代码语言:javascript复制
        <http auto-config="true" use-expressions="true">

                 ……

                 <remember-me/>

        </http>

(2)在登录表单中添加“name”等于“remember-me”的checkbox。

代码语言:javascript复制
<form action="${pageContext.request.contextPath}/account/checklogin" method="post">

            …

            <label class="fieldname">下次免登录</label>

            <input type="checkbox" name="remember-me" />

</form>

至此,当用户选择“记住登录状态”时,登录时将会多产生一个名为“remember-me”的长期cookie,用作下次免登录的标识。

不使用remember-me登录后的cookie

使用remember-me登录后的cookie

4 账户密码的散列加密

在实际应用中,我们往往需要对密码进行散列处理,以免被后台管理人员盗取,也就是说,后台服务器中记录的是经过hash算法加密的密码,而不是明码。

        Spring Security内置了密码散列的匹配功能,只要修改<authentication-manager>配置即可。

代码语言:javascript复制
<!-- 管理登录账号 -->

        <authentication-manager>

                 <authentication-provider user-service-ref="userDetailsService">

                 <!-- 密码使用md5散列加密 -->

                         <password-encoder hash="md5">

                         <!-- 使用UserDetails的username属性作为盐值 -->

                                  <salt-source user-property="username"/>

                         </password-encoder>

                 </authentication-provider>

        </authentication-manager>

这时,数据库中保存的数据就不是原来的明码密码了,而是散列之后的密码。

明码的密码值

散列加密的密码值

用户注册时,系统需要用散列算法对密码进行加密,可以使用Spring Security提供的“Md5PasswordEncoder”类实现。

以下为测试代码,使用UserDetails中的username作为密码的“盐值”(salt)。

代码语言:javascript复制
public class SpringMd5Test {

        @Test

        public void test(){

                 Md5PasswordEncoder md5 = new Md5PasswordEncoder();

                 String zhang3 = md5.encodePassword("123", "zhang3");

                 String li4 = md5.encodePassword("123", "li4");

                 String wang5 = md5.encodePassword("123", "wang5");

                 System.out.println(zhang3);

                 System.out.println(li4);

                 System.out.println(wang5);

        }

}

以下是输出的散列码。

0 人点赞