前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Spring Security 4入门

Spring Security 4入门

作者头像
用户10175992
发布2022-11-15 13:33:35
8370
发布2022-11-15 13:33:35
举报
文章被收录于专栏:辰远

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);

        }

}

以下是输出的散列码。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021-12-26,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档