SpringSecurity权限控制

初识SpringSecurity

学习思路

  • 了解SpringSecurity是什么。
  • 查看官网简介。
  • 简单快速阅读官方文档。

经过一段时间的学习,我们知道构建一个SpringBoot项目只需要三步:

  • 导入maven依赖。
  • 配置相关文件。
  • 编写测试代码。

安全框架

在Web开发中,安全一直是一个十分重要的环节。它是一种非功能性的需求,但是对于一个系统十分重要,我们一般都会使用一些组件或者框架去实现。
例如防御跨站脚本攻击,用户名密码验证这些功能,安全框架集成了一些类,可以自动帮我们做这些操作。
安全框架还为我们提供了用户授权功能,不同的用户角色可以操作的功能是不一样的,例如管理员可以拥有对数据增删查改的权限,普通用户只能查看一部分数据。
市面上比较知名的安全框架:

  • Shiro,用的十分多,功能十分强大。
  • SpringSecurity,用的也很多,但是功能没Shiro那么强大,但是它可以和Spring无缝结合,十分方便,基本的功能全都有。


    SpringSecurity官方文档位置

什么是SpringSecurity?

Spring Security是一个能够为基于Spring的企业应用系统提供声明式的安全访问控制解决方案的安全框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IoC,DI和AOP功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。

在SpringBoot项目中集成SpringSecurity

导入maven依赖。

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>

SpringSecurity的基本原理如下图所示。


SpringSecurity的基本原理

简单来说,SpringSecurity基本都是通过过滤器来完成配置的身份认证、权限认证以及登出。
SpringSecurity在Servlet的过滤链(FilterChain)中注册了一个过滤器FilterChainProxy,它会把请求代理到SpringSecurity维护的多个过滤链,每个过滤链会匹配一些URL,如果匹配则执行对应的过滤器。过滤链是有顺序的,一个请求只会执行第一条匹配的过滤链。
Spring Security 的配置本质上就是新增、删除、修改过滤器。

Http配置和密码验证

Http相关的配置,主要在HttpSecurity类中,在官方文档中有详细的说明。
HttpSecurity

用户密码验证的相关方法如下图所示。


用户密码验证相关方法

认证和授权

先来简单说说认证和授权的区别:

  • 认证(authentication),是验证一个特定操作的过程,这个操作通常情况下就是“登录”,需要操作者提供一些信息,认证提供方通过这些信息判断这个操作的来源确实是真正的操作者。用三个字概括就是“你是谁?”
  • 授权(authorization), 是授予某个用户、功能、节点、终端等一些特点的权限或者策略。从一个完整的权限管理系统包括操作、策略、角色、用户、用户组等。用五个字概括就是“你能干什么?”

编写代码

1、建立SpringBoot项目,导入SpringSecurity依赖。
2、导入前端资源。为了方便测试权限控制创建了三个不同等级的目录,里面分别放入了三个相同的html文件。


导入前端资源

为了实现不同的用户拥有不同的功能,我们的首页设计如下图所示。


首页

3、编写路由Controller。
@Controller
public class RouterController {

    @RequestMapping({"/","/index"})
    public String index(){
        return "index";
    }

    @RequestMapping("/toLogin")
    public String toLogin(){
        return "views/login";
    }

    //通过restful实现复用
    @RequestMapping("/level1/{id}")
    public String level1(@PathVariable("id") int id){
        return "views/level1/"+id;
    }

    @RequestMapping("/level2/{id}")
    public String level2(@PathVariable("id") int id){
        return "views/level2/"+id;
    }

    @RequestMapping("/level3/{id}")
    public String level3(@PathVariable("id") int id){
        return "views/level3/"+id;
    }
}

4、编写SpringSecurity配置类,继承WebSecurityConfigurerAdapter并在类上添加注解@EnableWebSecurity即可。

@EnableWebSecurity
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {}

5、启动项目,发现页面自动跳转到了登录页面。SpringSecurity默认提供的用户名是user,密码则在启动项目时在控制台中输出了。

security password

6、在配置类中重写以下方法,配置基本的请求过滤,对不同的请求定制授权规则。

@Override
protected void configure(HttpSecurity http) throws Exception {
    //配置不同的人访问内容不同,这里可以配置过滤器、登录注销规则、安全配置、OAuth2配置
    //我们平时只需要配置一些基本的规则即可

    //首页允许所有人访问
    http.authorizeRequests()
            //定制授权规则:哪些请求,哪些人可以访问
            .antMatchers("/").permitAll()
            .antMatchers("/level1/**").hasRole("vip1")
            .antMatchers("/level2/**").hasRole("vip2")
            .antMatchers("/level3/**").hasRole("vip3");
}

7、配置表单登录。当用户没有登录时,自动跳转到登录页面。

 //登录 /login跳转到登录页 /login?error 登录失败
//如果没有登录自动跳转到登录页面
http.formLogin()
        .usernameParameter("username")//配置用户名参数
        .passwordParameter("password")//配置密码参数
        .loginPage("/toLogin")//登录跳转页面请求
        .loginProcessingUrl("/login");//登录表单提交请求

8、我们来配置一些登录的用户进行测试。配置方法在源码中也有注释说明。


认证和授权配置源码注释
 //定义用户的认证规则(角色,密码...)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    //内存中定义用户信息
    //我们只定义了用户的密码加密,但是没有定义用户的认证规则的加密方式
    auth.inMemoryAuthentication()
            //使用的加密方式要对应下面密码的加密方式
            .passwordEncoder(new BCryptPasswordEncoder())
            //一个人可以拥有多个角色
            .withUser("coding")
            //密码的加密
            .password(new BCryptPasswordEncoder().encode("123456"))
            .roles("vip1","vip2")
       .and()
            .withUser("wunian")
            .password(new BCryptPasswordEncoder().encode("123456"))
            .roles("vip1","vip2","vip3")
       .and()
            .withUser("guest")
            .password(new BCryptPasswordEncoder().encode("123456"))
            .roles("vip1");
}

上述代码配置了三个用户的角色,并且对密码进行了加密处理,不同的角色能够访问的请求不一样,这个在步骤5中已经配置过了。
9、密码加密接口PasswordEncoder拥有很多的实现类,不同实现类对应了不同的加密方法,SpringSecurity推荐我们使用BCryptPasswordEncoder进行加密。


PasswordEncoder的实现类

Bcrypt密码加密源码

10、注销功能实现:

  • 配置前端注销按钮,注意这里应该使用post提交,否则点击注销按钮可能会出现404错误,因为SpringSecurity默认是防止CSRF 跨站伪请求的。
<!-- 将注销请求也改成post提交即可! -->
<form th:action="@{/logout}" method="post">
    <button type="submit">注销</button>
    <!--<a class="item" th:href="@{/logout}">-->
        <!--<i class="address card icon"></i> -->
    <!--</a>-->
</form>

也可以在配置类的configure(HttpSecurity http)方法中配置如下代码禁用防止CSRF跨站伪请求,但是不建议这样做,因为这会使得我们的系统不安全。

http.csrf().disable();

实际上SpringSecurity已经帮我们将全部的用户退出的规则都定义好了,在前端输入/logout请求会弹出如下代码提示。


logout请求代码提示
  • 在配置类的configure(HttpSecurity http)方法中配置登出成功的页面。
//注销 开启默认注销功能
http.logout().logoutSuccessUrl("/");//注销成功后跳转至首页
  • 重启项目测试登录和退出功能。

登录页定制记住我功能

功能需求

用户没登录时首页只显示导航栏,如果登录了,就显示该用户所拥有的角色权限能够看到的内容,即根据不同角色权限,前端显示不同的功能界面。
我们需要把SpringSecurity和Thymeleaf结合起来使用。

编写代码

1、导入Thymeleaf和SpringSecurity的整合包,版本一定要和SpringSecurity对应。

<dependency>
  <groupId>org.thymeleaf.extras</groupId>
  <artifactId>thymeleaf-extras-springsecurity5</artifactId>
  <version>3.0.4.RELEASE</version>
</dependency>

版本对应关系如下:

thymeleaf-extras-springsecurity3 for integration with Spring Security 3.x
thymeleaf-extras-springsecurity4 for integration with Spring Security 4.x
thymeleaf-extras-springsecurity5 for integration with Spring Security 5.x

2、修改前端页面。

  • 导入命名空间约束,如果使用IDEA导入不生效,可以尝试重启。
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
...
</html>
  • 修改前端页面。注意以下几个属性:
    sec:authorize="isAuthenticated()":判断用户是否登录。
    sec:authentication="principal.username":获取当前登录用户的用户名。
    sec:authentication="principal.authorities":获取当前登录用户的所有角色。
    sec:authorize="hasRole('xxx')":判断当前登录用户是否含有某个角色权限。
<!DOCTYPE html>
<html lang="en"
      xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"><!--导入springsecurity5命名空间-->

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
    <title>首页</title>
    <!--semantic-ui-->
    <link href="https://cdn.bootcss.com/semantic-ui/2.4.1/semantic.min.css" rel="stylesheet">
    <link th:href="@{/qinjiang/css/qinstyle.css}" rel="stylesheet">
</head>
<body>
    <!--主容器-->
    <div class="ui container">
        <div class="ui segment" id="index-header-nav" th:fragment="nav-menu">
            <div class="ui secondary menu">
                <a class="item" th:href="@{/index}">首页</a>
    
                <!--登录注销-->
                <div class="right menu">
    
                    <!--核心类:Authentication-->
                    <!-- 如果未登录就显示登陆按钮 -->
                    <div sec:authorize="!isAuthenticated()">
                        <a class="item" th:href="@{/toLogin}">
                            <i class="address card icon"></i> 登录
                        </a>
                    </div>
    
                    <!-- 如果已登录,显示用户的信息 -->
                    <div sec:authorize="isAuthenticated()">
                        <a class="item">
                            <!--<i class="address card icon"></i>-->
                            用户名:<span sec:authentication="principal.username"></span> &nbsp;
                            角色:<span sec:authentication="principal.authorities"></span>
                        </a>
                    </div>
    
                    <div sec:authorize="isAuthenticated()">
                        <!-- 将注销请求也改成post提交即可! -->
                        <form th:action="@{/logout}" method="post">
                            <button type="submit">注销</button>
                            <!--<a class="item" th:href="@{/logout}">-->
                            <!--<i class="address card icon"></i> -->
                            <!--</a>-->
                        </form>
                    </div>
                </div>
            </div>
        </div>
    
        <div class="ui segment" style="text-align: center">
            <h3>Spring Security Study by 秦疆</h3>
        </div>
    
        <div>
            <br>
            <div class="ui three column stackable grid">
                <div sec:authorize="hasRole('vip1')">
                    <div class="column">
                        <div class="ui raised segment">
                            <div class="ui">
                                <div class="content">
                                    <h5 class="content">Level 1</h5>
                                    <hr>
                                    <div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
                                    <div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
                                    <div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
    
                <div sec:authorize="hasRole('vip2')">
                    <div class="column">
                        <div class="ui raised segment">
                            <div class="ui">
                                <div class="content">
                                    <h5 class="content">Level 2</h5>
                                    <hr>
                                    <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                                    <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                                    <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
    
                <div sec:authorize="hasRole('vip3')">
                    <div class="column">
                        <div class="ui raised segment">
                            <div class="ui">
                                <div class="content">
                                    <h5 class="content">Level 3</h5>
                                    <hr>
                                    <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                                    <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                                    <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
                                </div>
                            </div>
                        </div>
                    </div>
                </div>
            </div>
        </div>
    </div>
    <script th:src="@{/qinjiang/js/jquery-3.1.1.min.js}"></script>
    <script th:src="@{/qinjiang/js/semantic.min.js}"></script>
</body>
</html>

3、记住我功能实现:

  • 一旦关闭浏览器,用户Session就没有了,我们需要实现关闭后用户还可以直接登录,通过在浏览器添加Cookie来实现。
  • 前端配置,添加一个复选框按钮,name设置为"remember"。
<input type="checkbox" name="remember"> 记住我
  • 后台配置,在配置类configure(HttpSecurity http)方法中配置rememberMe参数为"remember"。
 //记住我功能
 //自定义的登录页需要配置rememberMe的参数名,这样就可以绑定到前端
http.rememberMe().rememberMeParameter("remember");

4、首页定制,在配置类configure(HttpSecurity http)方法中添加如下配置即可。

http.formLogin() 
  .usernameParameter("username") // 前端用户名提交参数         
  .passwordParameter("password") // 前端密码提交参数 
  .loginPage("/toLogin") //登录跳转页面请求
  .loginProcessingUrl("/login"); // 登陆表单提交请求
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,084评论 6 503
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,623评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,450评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,322评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,370评论 6 390
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,274评论 1 300
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,126评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,980评论 0 275
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,414评论 1 313
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,599评论 3 334
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,773评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,470评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,080评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,713评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,852评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,865评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,689评论 2 354

推荐阅读更多精彩内容