阅读 479

(七) SpringBoot起飞之路-整合SpringSecurity(Mybatis、JDBC、内存)

兴趣的朋友可以去了解一下前几篇,你的赞就是对我最大的支持,感谢大家!

  • 文章目录

(一) SpringBoot起飞之路-HelloWorld

juejin.im/post/684490…

(二) SpringBoot起飞之路-入门原理分析

juejin.im/post/684490…

(三) SpringBoot起飞之路-YAML配置小结(入门必知必会)

juejin.im/post/684490…

(四) SpringBoot起飞之路-静态资源处理

juejin.im/post/684490…

(五) SpringBoot起飞之路-Thymeleaf模板引擎

juejin.im/post/684490…

(六) SpringBoot起飞之路-整合JdbcTemplate-Druid-MyBatis

juejin.im/post/684490…

说明:

  • 这一篇的目的还是整合,也就是一个具体的实操体验,原理性的没涉及到,我本身也没有深入研究过,就不献丑了

  • SpringBoot 起飞之路 系列文章的源码,均同步上传到 github 了,有需要的小伙伴,随意去 down

  • 才疏学浅,就会点浅薄的知识,大家权当一篇工具文来看啦,不喜勿愤哈 ~

(一) 初识 Spring Security

(1) 引言

权限以及安全问题,虽然并不是一个影响到程序、项目运行的必须条件,但是却是开发中的一项重要考虑因素,例如某些资源我们不想被访问到或者我们某些方法想要满足指定身份才可以访问,我们可以使用 AOP 或者过滤器来实现要求,但是实际上,如果代码涉及的逻辑比较多以后,代码是极其繁琐,冗余的,而有很多开发框架,例如 Spring Security,Shiro,已经为我们提供了这种功能,我们只需要知道如何正确配置以及使用它了

(2) 基本介绍

先看一下官网的介绍

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.

Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于spring的应用程序的实际标准。

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它很容易扩展以满足定制需求

简单的说,Spring Security 就是一个控制访问权限,强大且完善的框架

Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,同时它们也是 Spring Security 提供的核心功能

用户认证:用户认证就是指这个用户身份是否合法,一般我们的用户认证就是通过校验用户名密码,来判断用户身份的合法性,确定身份合法后,用户就可以访问该系统

用户授权:如果不同的用户需要有不同等级的权限,就涉及到用户授权,用户授权就是对用户能访问的资源,所能执行的操作进行控制,根据不同用户角色来划分不同的权限

(二) 静态页面导入 And 环境搭建

(1) 关于静态页面

A:页面介绍

页面是我自己临时弄得,有需要的朋友可以去我 GitHub:ideal-20 下载源码,简单说明一下这个页面

做一个静态页面如果嫌麻烦,也可以单纯的自己创建一些简单的页面,写几个标题文字,能体现出当前是哪个页面就好了

我代码中用的这些页面,就是拿开源的前端组件框架进行了一点的美化,然后方便讲解一些功能,页面模板主要是配合 Thymeleaf

1、目录结构

├── index.html                        // 首页
├── images                            // 首页图片,仅美观,无实际作用
├── css                               // 上线项目文件,放在服务器即可正常访问
├── js                                // 项目截图
├── views                             // 总子页面文件夹,权限验证的关键页面
│   ├── login.html					  // 自制登录页面(用来替代 Spring Security 默认的 )
│   ├── L-A							  // L-A 子页面文件夹,下含 a b c 三个子页面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|	├── L-B							  // L-B 子页面文件夹,下含 a b c 三个子页面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
|	├── L-C							  // L-C 子页面文件夹,下含 a b c 三个子页面
│   │   ├── a.html
│   │   ├── b.html
│   │   ├── c.html
复制代码

B:导入到项目

主要就是把基本一些链接,引入什么的先替换成 Thymeleaf 的标签格式,这里语法用的不是特别多,即使对于 Thymeleaf 不是很熟悉也是很容易看懂的,当然如果仍然感觉有点吃力,可以单纯的做成 html,将就一下,或者去看一下我以前的文章哈,里面有关于 Thymeleaf 入门的讲解

css、image、js 放到 resources --> static 下 ,views 和 index.html 放到 resources --> templates下

(2) 环境搭建

A:引入依赖

这一部分引入也好,初始化项目的时候,勾选好自动生成也好,只要依赖正常导入了即可

  • 引入 Spring Security 模块
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
复制代码

关键的依赖主要就是上面这个启动器,但是还有一些就是常规或者补充的了,例如 web、thymeleaf、devtools

thymeleaf-extras-springsecurity5 这个后面讲解中会提到,是用来配合 Thymeleaf 整合 Spring Security 的

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

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

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
复制代码

B:页面跳转 Controller

因为我们用了模板,页面的跳转就需要交给 Controller 了,很简单,首先是首页的,当然关于页面这个就无所谓了,我随便跳转到了我的博客,接着还有一个登录页面的跳转

有一个小 Tip 需要提一下,因为 L-A、L-B、L-C 文件夹下都有3个页面 a.html 、b.html 、c.html,所以可以利用 @PathVariable 写一个较为通用的跳转方法

@Controller
public class PageController {

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

    @RequestMapping("/about")
    public String toAboutPage() {
        return "redirect:http://www.ideal-20.cn";
    }

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

    @RequestMapping("/levelA/{name}")
    public String toLevelAPage(@PathVariable("name") String name) {
        return "views/L-A/" + name;
    }

    @RequestMapping("/levelB/{name}")
    public String toLevelBPage(@PathVariable("name") String name) {
        return "views/L-B/" + name;
    }

    @RequestMapping("/levelC/{name}")
    public String toLevelCPage(@PathVariable("name") String name) {
        return "views/L-C/" + name;
    }
}
复制代码

C:环境搭建最终效果

  • 为了贴图方便,我把页面拉窄了一点
  • 首页右上角应该为登录的链接,这里是因为,我运行的是已经写好的代码,不登录页面例如 L-A-a 等模块就显示不出来,所以拿一个定义好的管理员身份登陆了
  • 关于如何使其自动切换显示登陆还是登录后信息,在后面会讲解

1、首页

2、子页面

L-A、L-B、L-C 下的 a.html 、b.html 、c.html 都是一样的,只是文字有一点变化

3、登陆页面

(三) 整合 Spring Security (内存中)

这一部分,为了简化一些,容易理解一些,没有从带数据的场景出发(因为涉及代码少一些,所以讲解会多一点),而是直接将一些身份等等写死了,写到了内存中,方便理解,接着会在下一个标题中给出含有数据库的写法(讲解会少一些,重点只说一些与前一种的不同点)

(1) 配置授权内容

A:源码了解用户授权方式

可以去官网看一下,官网有提供给我们一些样例,其中有一个关于配置类的小样例,也就是下面这个,我们通过这个例子,展开分析

https://docs.spring.io/spring-security/site/docs/5.3.2.RELEASE/reference/html5/#jc-custom-dsls

@EnableWebSecurity
public class Config extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .apply(customDsl())
                .flag(true)
                .and()
            ...;
    }
}
复制代码

1、创建 config --> SecurityConfig 配置类

  • 创建一个配置类,像官网中一样,继承 WebSecurityConfigurerAdapter
  • 类上添加 @EnableWebSecurity 注解,代表开启WebSecurity模式
  • 重写 configure(HttpSecurity http) 方法
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
	@Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }
}
复制代码

既然是重写,那么我们可以点进去,看一下父类中关于 configure(HttpSecurity http) 方法的源码注释,它有很多有用的信息

我摘选出这么两小段,第一段的意思就是 ,我们想要使用 HttpSecurity ,要通过重写,不能通过 super 调用,否则会有覆盖问题,第二段就是给出了一个默认的配置方式

* Override this method to configure the {@link HttpSecurity}. Typically subclasses
* should not invoke this method by calling super as it may override their

* configuration. The default configuration is:
* http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
复制代码

2、按照源码的注释分析

我们先按照刚才看到的注释写出来,首先能看到,它是支持一个链式调用的

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .anyRequest().authenticated()
            .and().formLogin()
            .and().httpBasic();
}
复制代码
  • 通过字面意思也很好理解,authorizeRequests 是关于请求授权的,所以要涉及到关于请求授权(允许指定身份用户访问不同权限的资源)的问题就需要调用了

  • 其次,anyRequest().authenticated() 也就是说所有HTTP请求都需要被认证

  • 接着看,通过 and() 连接了一些新的内容,例如选择表单登录还是 HTTPBasic 的方式(这里认证的过程就是让你输入用户名密码,检测你的身份,两种方式表单或者那种弹窗)

Basic认证是一种较为简单的HTTP认证方式,客户端通过明文(Base64编码格式)传输用户名和密码到服务端进行认证,通常需要配合HTTPS来保证信息传输的安全

给大家演示一下:

  • 如果不指定一种认证方式 .and().formLogin() 或者 .and().httpBasic() 访问任何页面都会提示 403 禁止访问的错误
  • 指定 .and().formLogin() 认证,弹出一个表单页面(自带的,和自己创建的没关系)
  • 指定 .and().httpBasic(); 认证,弹出一个窗口进行 HTTPBasic 认证

B:自定制用户授权

1、先看源码注释

默认配置,设定了所有 HTTP 请求 都需要进行认证,所以我们在访问首页等的时候也会被拦截,但是实际情况下,有一些页面是可以被任何人访问的,例如首页,或者自定义的登陆的等页面,这时候需要用自己定义一些用户授权的规则

在 WebSecurityConfigurerAdapter 的 formLogin() 注释附近,又看到了一个有意思的内容

注:&quot 代表引号

* 		http
* 			.authorizeRequests(authorizeRequests ->
* 				authorizeRequests
* 					.antMatchers(&quot;/**&quot;).hasRole(&quot;USER&quot;)
* 			)
复制代码

这就是我们想要找的,自定义的配置,通过一个一个 antMatchers 进行匹配,通过 hasRole 来规定其合法的身份,也就是说只有满足这个身份的用户才能访问前面规定的路径资源

Matchers 前面的 ant 前缀代表着,他可以用 ant 风格的路径表达式(举例的时候就能看懂了)

通配符 说明
? 匹配任何单字符
* 匹配0或者任意数量的字符
** 匹配0或者更多的目录

补充: 如果想用正则表达式的方式,可以用这个方法 .regexMatchers()

当然了,有很多情况下,你想要让任何人都可以访问某个路径,例如首页,permitAll() 方法 就可以达到这种效果,在这里补充一些常用的方法

  • permitAll():允许任何访问

  • denyAll():拒绝所有访问

  • anonymous():允许匿名用户访问

  • authenticated():允许认证的用户进行访问

  • hasRole(String) :如果用户具备给定角色(用户组)的话,就允许访问/

  • hasAnyRole(String…) :如果用户具有给定角色(用户组)中的一个的话,允许访问.

  • rememberMe() :如果用户是通过Remember-me功能认证的,就允许访问

  • fullyAuthenticated():如果用户是完整认证的话(不是通过Remember-me功能认证的),就允许访问

  • hasAuthority(String):如果用户具备给定权限的话就允许访问

  • hasAnyAuthority(String…) :如果用户具备给定权限中的某一个的话,就允许访问

  • hasIpAddress(String):如果请求来自给定ip地址的话,就允许访问.

  • not() :对其他访问结果求反

说明:hasAnyAuthority("ROLE_ADMIN") 和 hasRole("ADMIN") 的区别就是,后者会自动使用 它会自动使用 “ROLE_” 前缀

2、我们来定制一下用户授权

@Override
protected void configure(HttpSecurity http) throws Exception {
	http.authorizeRequests()
        	.antMatchers("/").permitAll()
        	.antMatchers("/levelA/**").hasRole("vip1")
        	.antMatchers("/levelB/**").hasRole("vip2")
        	.antMatchers("/levelC/**").hasRole("vip3")
        	.and().formLogin();
}
复制代码

我们上面代码的意思就是,当访问 /levelA/ /levelB/ /levelC/ 这三个路径下面的任意文件(这里有 a/b/c.html)都需要认证,身份分别是对应 vip1、vip2、vip3,而其他页面,就可以随便访问了

很显然,虽然说规定了授权的内容,也就是哪些权限的用户,可以访问哪些资源,但是我们由于并没有配置用户的信息(合法的或者非法的),所以自然,前面的登录页面,都是会直接报错的,下面我们来分析一下,如何进行认证

(2) 配置认证内容

A:源码了解用户认证方式

刚才的授权部分,我们重写了 configure(HttpSecurity http) 方法,我们继续看看重写方法中,有没有可能帮助我们验证身份,进行用户认证的方法,我们首先来看这个方法 configure(AuthenticationManagerBuilder auth)

先去看一下源码的注释(此部分的格式,我稍微修改了一下,方便观看):

这是其中他局举的一个例子,其实这个就是我们想要的,看注释也可以看出来,他就是用来在内存中启用基于用户名的身份验证的

* protected void configure(AuthenticationManagerBuilder auth) {
*  auth
*  // enable in memory based authentication with a user named
*  // &quot;user&quot; and &quot;admin&quot;
*  		.inMemoryAuthentication()
*   		.withUser(&quot;user&quot;)
*    			.password(&quot;password&quot;)
*    			.roles(&quot;USER&quot;).and()
*        	.withUser(&quot;admin&quot;)
*    			.password(&quot;password&quot;)
*    			.roles(&quot;USER&quot;, &quot;ADMIN&quot;);
* }
复制代码

照猫画虎,我们也先这么做

B:自定制用户认证

代码如下:

//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
            .withUser("admin")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1", "vip2", "vip3")
            .and()
            .withUser("ideal-20")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1", "vip2")
            .and()
            .withUser("jack")
        		.password(new BCryptPasswordEncoder().encode("666"))
        		.roles("vip1");
}
复制代码

我们就是照着例子打的,但是,其中我们又加入了编码的问题,它要求必须进行编码,否则会报错,官方推荐的是bcrypt加密方式,我们这里就用这种,当然自己用常见的 MD5 等等都是可以的,可以自己写一个工具类

到这里,测试一下,实际上就可以按照身份的不同,从而拥有访问不同路径资源你的权限了,主要的功能已经实现了,下面补充一些,更加友好的功能,例如登录注销按钮的显示,以及记住密码等等

(3) 注销问题

1、注销配置

当然了,前面因为已经有很多配置了,所以可以通过 .and() 进行连接,例如 .and().xxx,或者像下面给出的,单独再写一个 http.xxx

@Override
protected void configure(HttpSecurity http) throws Exception {
   ......
    // 注销配置
	http.logout().logoutSuccessUrl("/")
}
复制代码

上面短短一句的代码, logout() 代表开启了注销的配置,logoutSuccessUrl("/"),代表注销成功后,返回的页面,我们令其注销后回到首页

前台的页面中,我已经给出了注销的按钮的代码,当然这不是固定的,不同的 ui 框架,不同的模板引擎都是不一样的,但是路径是 /logout

<a class="item" th:href="@{/logout}">
  <i class="address card icon"></i> 注销
</a>
复制代码

(4) 根据身份权限显示组件

A:登录、注销的显示

还有这样一种问题,右上角,未登录的时候,应该显示登陆按钮,登录后,应该显示用户信息,以及注销等等,这一部分,主要是页面这边的问题

显示的条件其实很简单,就是判断是否认证了,认证了就取出一些值,没认证就显示登陆

1、这时,我们就需要引入一个 Thymeleaf 配合 Spring Security 的一个依赖 (当然了如果是别的技术,就不一样了)

地址如下:

https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity5

<dependency>
    <groupId>org.thymeleaf.extras</groupId>
    <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    <version>3.0.4.RELEASE</version>
</dependency>
复制代码

2、导入命名空间

引入这个文件的目的,就是为了在页面写权限判断等相关的内容的时候可以有提示

<html lang="en" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
复制代码

3、修改导航栏逻辑

<!--登录注销-->
<div class="right menu">

  <!--如果未登录-->
  <div sec:authorize="!isAuthenticated()">
    <a class="item" th:href="@{/toLoginPage}">
      <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>
      <!--角色:<span sec:authentication="principal.authorities"></span>-->
    </a>
  </div>

  <div sec:authorize="isAuthenticated()">
    <a class="item" th:href="@{/logout}">
      <i class="address card icon"></i> 注销
    </a>
  </div>
</div>
复制代码

B:组件面板的显示

上面的代码,解决了导航栏的问题,但是例如我们首页中,一些板块,对于不同的用户的显示也是不同的吗

正如上面的例子,没有登录的用户,是不能访问了 /levelA/、 /levelB/、 /levelC/ 下面的任何文件的,只有登录的用户,根据权限的大小,才能访问某一个,或者所有

而我们首页部分的三个面板就是用来显示这三块的链接,对于没有足够身份的人,实际上显示这个面板就已经是多余了,当然,你可以选择显示,但是如果想要根据身份显示面板怎么做呢?

关键就是在 div 中添加了这样一句权限的代码,没有这个指定的身份,这个面板就不会显示sec:authorize="hasRole('vip1')"

<div class="column" sec:authorize="hasRole('vip1')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelA/a}">L-A-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelA/b}">L-A-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelA/c}">L-A-c</a>
    </div>
  </div>
</div>
<div class="column" sec:authorize="hasRole('vip2')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelB/a}">L-B-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelB/b}">L-B-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelB/c}">L-B-c</a>
    </div>
  </div>
</div>
<div class="column" sec:authorize="hasRole('vip3')">
  <div class="ui raised segments">
    <div class="ui segment">
      <a th:href="@{/levelC/a}">L-A-a</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelC/b}">L-C-b</a>
    </div>
    <div class="ui segment">
      <a th:href="@{/levelC/c}">L-C-c</a>
    </div>
  </div>
</div>
复制代码

演示一下:

(5) 记住用户

如果重启浏览器后,就需要重新登录,对于一部分用户来说,他们认为是麻烦的,所以很多网站登录时都提供记住用户这种选项

1、一个简单的配置就可以达到目的,这种情况下,默认的登陆页面,就会多出一个记住用户的单选框

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	//记住用户
    http.rememberMe();
}
复制代码

2、但是如果,登陆页面是自定义(下面讲)的怎么办呢?,其实只要修改为如下配置即可,

//定制记住我的参数!
http.rememberMe().rememberMeParameter("remember");
复制代码

上面的 remember 对应 input 中的 name 属性值

<input type="checkbox" name="remember"/>
<label>记住密码</label>
复制代码

3、它做了哪些事情呢?

可以打开页面的控制台看一下,实际上配置后,用户选择记住密码后,会自动帮我们增加一个 cookie 叫做 remember-me,过期时间为 14 天,当注销的时候,这个 cookie 就会被删除了

(6) 定制登录页面

1、配置

自带的登陆页面确实,还是比较丑的,版本更低一些的,更是不美观,如果想要使用自己定制的登陆页面,可以加入下面的配置

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	// 登陆表单提交请求
    http.formLogin()
	.usernameParameter("username")
	.passwordParameter("password")
	.loginPage("/toLoginPage")
	.loginProcessingUrl("/login")
}
复制代码
  • .loginPage("/toLoginPage") 就是说,当你访问一些需要用户权限认证的页面时,就会发起这个请求,到你的登录页面
  • .loginProcessingUrl("/login") 就是表单中,真正要提交请求的一个路径
  • 其余两个就是关于用户名和密码的一个获取,其值和页面中用户名密码的 name 属性值一致

2、页面跳转

前面我们就提过这个,回顾一下

@RequestMapping("/toLoginPage")
public String toLoginPage() {
    return "views/login";
}
复制代码

3、自定义登录页面的表单提交 action 设置

<form id="login" class="ui fluid form segment" th:action="@{/login}" method="post">
	......
</form>
复制代码

(7) 关闭csrf

@Override
protected void configure(HttpSecurity http) throws Exception {
	......
	//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
	http.csrf().disable();
}
复制代码

(四) 整合 Spring Security (JDBC)

因为配置内存中的用户还是相对简单一些的,所以一些细节也都说了一下,基于上面的基础,来看一下 如何用 JDBC 实现上面的功能,当然了这部分只能算补充,基本不会这么用的,下面的整合 MyBatis 才是常用的()

(1) 创建表以及数据

这里创建了三个字段,用户名,密码,还有角色,插入数据的时候密码是使用了 md5 加密(自己写了一个工具类)

这里更合理了一些,我把权限定义为了普通用户、普通管理员、超级管理员(自己设计都行)

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  `roles` varchar(255) DEFAULT NULL COMMENT '角色',
  PRIMARY KEY (`uid`)
)

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'superadmin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_SUPER_ADMIN');
INSERT INTO `user` VALUES (2, 'admin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_ADMIN');
INSERT INTO `user` VALUES (3, 'ideal-20', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_USER');
复制代码

(2) 创建实体

我使用了 lombok,不过自己写 get set 构造方法 也是一样的

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private Integer uid;
    private String username;
    private String password;
    private String roles;
}
复制代码

(3) 配置授权内容

这部分没什么区别

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/levelA/**").hasAnyRole("USER","ADMIN","SUPER_ADMIN")
            .antMatchers("/levelB/**").hasAnyRole("ADMIN","SUPER_ADMIN")
            .antMatchers("/levelC/**").hasRole("SUPER_ADMIN")
            .and().formLogin()

            // 登陆表单提交请求
            .and().formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/toLoginPage")
            .loginProcessingUrl("/login")
            //注销
            .and().logout().logoutSuccessUrl("/")
            //记住我
            .and().rememberMe().rememberMeParameter("remember")
            //关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
            .and().csrf().disable();
}
复制代码

(4) 配置认证内容

A:配置数据库

spring:
  datasource:
    username: root
    password: root99
    url: jdbc:mysql://localhost:3306/springboot_security_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

server:
  port: 8082
复制代码

B:具体配置

以几个注意的地方:

  • 查询语句都是通过 username 查询

  • usersByUsernameQuery()方法里的参数一定要有一个 true 的查询结果,所以我直接在查询语句中写了一个 true

  • MD5 工具类,是我以前一个项目中整理的,加盐的部分,我给注释掉了,因为我测试的时候简单点

  • DataSource dataSource 要在前面注入进来(选择 sql 的)

//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

auth.jdbcAuthentication()
            .dataSource(dataSource)
            .usersByUsernameQuery("select username,password,true from user where username = ?")
            .authoritiesByUsernameQuery("select username,roles from user where username = ?")
            .passwordEncoder(new PasswordEncoder() {
                @Override
                public String encode(CharSequence rawPassword) {
                    return MD5Util.MD5EncodeUtf8((String) rawPassword);
                }

                @Override
                public boolean matches(CharSequence rawPassword, String encodedPassword) {
                    return encodedPassword.equals(MD5Util.MD5EncodeUtf8((String) rawPassword));
                }
            });
}
复制代码

C:MD5工具类

package cn.ideal.utils;

import java.security.MessageDigest;

/**
 * @ClassName: MD5Util
 * @Description: MD5 加密工具类
 * @Author: BWH_Steven
 * @Date: 2020/4/27 16:46
 * @Version: 1.0
 */
public class MD5Util {

    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * 返回大写MD5
     *
     * @param origin
     * @param charsetname
     * @return
     */
    private static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
        } catch (Exception exception) {
        }
        return resultString.toUpperCase();
    }

    public static String MD5EncodeUtf8(String origin) {
//        origin = origin + PropertiesUtil.getProperty("password.salt", "");
        return MD5Encode(origin, "utf-8");
    }

    private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
}
复制代码

D:修改页面

到这里,JDBC 的整合方式就成功了,至于前面的页面只需要根据我们自己设计的权限进行修改,别的地方和前面内存中的方式是一样的

<div class="ui stackable three column grid">
  <div class="column" sec:authorize="hasAnyRole('USER','ADMIN','SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelA/a}">L-A-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelA/b}">L-A-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelA/c}">L-A-c</a>
      </div>
    </div>
  </div>
  <div class="column" sec:authorize="hasAnyRole('ADMIN','SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelB/a}">L-B-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelB/b}">L-B-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelB/c}">L-B-c</a>
      </div>
    </div>
  </div>
  <div class="column" sec:authorize="hasRole('SUPER_ADMIN')">
    <div class="ui raised segments">
      <div class="ui segment">
        <a th:href="@{/levelC/a}">L-C-a</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelC/b}">L-C-b</a>
      </div>
      <div class="ui segment">
        <a th:href="@{/levelC/c}">L-C-c</a>
      </div>
    </div>
  </div>
  <!-- <div class="column"></div> -->
</div>
复制代码

(五) 整合 Spring Security (MyBatis)

因为这部分内容是比较常用的,所以,我尽可能给的完善一些

(1) 添加依赖

像 lombok、commons-lang3 都不是必须的,都是可以使用原生的一些手段替代的,写到那里我会提的

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

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

<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.2</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
    <optional>true</optional>
</dependency>
复制代码

(2) 创建表

和 JDBC 部分用同样的表

三个字段,用户名,密码,还有角色,插入数据的时候密码是使用了 md5 加密(自己写了一个工具类)

这里更合理了一些,我把权限定义为了普通用户、普通管理员、超级管理员(自己设计都行)

-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user`  (
  `uid` int(11) NOT NULL AUTO_INCREMENT,
  `username` varchar(255) DEFAULT NULL COMMENT '用户名',
  `password` varchar(255) DEFAULT NULL COMMENT '密码',
  `roles` varchar(255) DEFAULT NULL COMMENT '角色',
  PRIMARY KEY (`uid`)
)

-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'superadmin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_SUPER_ADMIN');
INSERT INTO `user` VALUES (2, 'admin', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_ADMIN');
INSERT INTO `user` VALUES (3, 'ideal-20', 'FAE0B27C451C728867A567E8C1BB4E53', 'ROLE_USER');
复制代码

(3) 整合 MyBatis

在进行 Spring Security 的配置前,最好先把 MyBatis 先整合好,这样等会只考虑 Spring Security 的问题就可以了

说明:这部分我尽可能简化了,例如连接池就用默认的,如果这部分感觉还是有点问题,可以参考一下我前几篇,关于整合 MyBatis 的文章

A:配置数据库

spring:
  datasource:
    username: root
    password: root99
    url: jdbc:mysql://localhost:3306/springboot_security_test?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver

mybatis:
  mapper-locations: classpath:mapper/*Mapper.xml
  type-aliases-package: cn.ideal.pojo

server:
  port: 8081
复制代码

B:配置 Mapper 以及 XML

UserMapper

@Mapper
public interface UserMapper {
    User queryUserByUserName(String username);
}
复制代码

mapper/UserMapper.xml

<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="cn.ideal.mapper.UserMapper">
    <select id="queryUserByUserName" parameterType="String" resultType="cn.ideal.pojo.User">
         select * from user where username = #{username}
    </select>
</mapper>
复制代码

这里就不演示测试了,是没有问题的

(4) 配置授权内容

这部分没什么好说的,和前面的都一样,解释在内存中配置用户时已经详细说过了

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
            .antMatchers("/").permitAll()
            .antMatchers("/levelA/**").hasAnyRole("USER","ADMIN","SUPER_ADMIN")
            .antMatchers("/levelB/**").hasAnyRole("ADMIN","SUPER_ADMIN")
            .antMatchers("/levelC/**").hasRole("SUPER_ADMIN")
            .and().formLogin()

            // 登陆表单提交请求
            .and().formLogin()
            .usernameParameter("username")
            .passwordParameter("password")
            .loginPage("/toLoginPage")
            .loginProcessingUrl("/login")
            //注销
            .and().logout().logoutSuccessUrl("/")
            //记住我
            .and().rememberMe().rememberMeParameter("remember")
            //关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
            .and().csrf().disable();
}
复制代码

(5) 配置认证内容

A:创建 UserService

创建一个类,实现 UserDetailsService,其实主要就是为了 loadUserByname 方法,在这个类中,我们可以注入 mapper 等等,去查用户,如果查不到,就还留在这个页面,如果查到了,做出一定逻辑后(例如判空等等),就会把用户信息封装到 Spring Security 自己的的 User类中去,Spring Security 拿前台的数据和它比较,做出操作,例如认证成功或者错误

注意:

  • StringUtils 是 commons.lang3 下的,使用需要导包,我们用了一个判空功能,不想用的话,用原生的是一个道理,这不是重点
  • 注意区分自己的 User 和 Spring Security 的 User
@Service
public class UserService<T extends User> implements UserDetailsService{

    @Autowired
    private UserMapper userMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userMapper.queryUserByUserName(username);
        if (username == null){
            throw  new UsernameNotFoundException("用户名不存在");
        }

        List<SimpleGrantedAuthority> authorityList = new ArrayList<>();
        String role = user.getRoles();
        if (StringUtils.isNotBlank(role)){
            authorityList.add(new SimpleGrantedAuthority(role.trim()));
        }
        return new org.springframework.security.core.userdetails
            .User(user.getUsername(),user.getPassword(),authorityList);
    }
}
复制代码

B:修改配置类

这里也很熟悉,我们调用就可以调用 userDetailsService 了,同样还需要指定编码相关的内容 实例化 PasswordEncoder,就需要重写 encode、 matches

//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userService).passwordEncoder(new PasswordEncoder() {
        @Override
        public String encode(CharSequence rawPassword) {
            return MD5Util.MD5EncodeUtf8((String) rawPassword);
        }

        @Override
        public boolean matches(CharSequence rawPassword, String encodedPassword) {
            return encodedPassword.equals(MD5Util.MD5EncodeUtf8((String) rawPassword));
        }
    });
}
复制代码

C:MD5 工具类补充

其实上面已经给出了,但是怕大家看起来不方便,这里再贴一下

MD5 工具类,是我以前一个项目中整理的,加盐的部分,我给注释掉了,因为我测试的时候可以简单点

package cn.ideal.utils;

import java.security.MessageDigest;

/**
 * @ClassName: MD5Util
 * @Description: MD5 加密工具类
 * @Author: BWH_Steven
 * @Date: 2020/4/27 16:46
 * @Version: 1.0
 */
public class MD5Util {

    private static String byteArrayToHexString(byte b[]) {
        StringBuffer resultSb = new StringBuffer();
        for (int i = 0; i < b.length; i++)
            resultSb.append(byteToHexString(b[i]));

        return resultSb.toString();
    }

    private static String byteToHexString(byte b) {
        int n = b;
        if (n < 0)
            n += 256;
        int d1 = n / 16;
        int d2 = n % 16;
        return hexDigits[d1] + hexDigits[d2];
    }

    /**
     * 返回大写MD5
     *
     * @param origin
     * @param charsetname
     * @return
     */
    private static String MD5Encode(String origin, String charsetname) {
        String resultString = null;
        try {
            resultString = new String(origin);
            MessageDigest md = MessageDigest.getInstance("MD5");
            if (charsetname == null || "".equals(charsetname))
                resultString = byteArrayToHexString(md.digest(resultString.getBytes()));
            else
                resultString = byteArrayToHexString(md.digest(resultString.getBytes(charsetname)));
        } catch (Exception exception) {
        }
        return resultString.toUpperCase();
    }

    public static String MD5EncodeUtf8(String origin) {
//        origin = origin + PropertiesUtil.getProperty("password.salt", "");
        return MD5Encode(origin, "utf-8");
    }

    private static final String hexDigits[] = {"0", "1", "2", "3", "4", "5",
            "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
}
复制代码

(六) 结尾

如果文章中有什么不足,欢迎大家留言交流,感谢朋友们的支持!

如果能帮到你的话,那就来关注我吧!如果您更喜欢微信文章的阅读方式,可以关注我的公众号

在这里的我们素不相识,却都在为了自己的梦而努力 ❤

一个坚持推送原创开发技术文章的公众号:理想二旬不止