狂神说java系列视频
资源来源:微信公众号——狂神说,boot系列文章
12、集成SpringSecurity
安全简介
在 Web 开发中,安全一直是非常重要的一个方面。安全虽然属于应用的非功能性需求,但是应该在应用开发的初期就考虑进来。如果在应用开发的后期才考虑安全的问题,就可能陷入一个两难的境地:一方面,应用存在严重的安全漏洞,无法满足用户的要求,并可能造成用户的隐私数据被攻击者窃取;另一方面,应用的基本架构已经确定,要修复安全漏洞,可能需要对系统的架构做出比较重大的调整,因而需要更多的开发时间,影响应用的发布进程。因此,从应用开发的第一天就应该把安全相关的因素考虑进来,并在整个应用的开发过程中。
市面上存在比较有名的:Shiro,Spring Security !
这里需要阐述一下的是,每一个框架的出现都是为了解决某一问题而产生了,那么Spring Security框架的出现是为了解决什么问题呢?
首先我们看下它的官网介绍:Spring Security官网地址
原文:
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 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是一个功能强大且高度可定制的身份验证和访问控制框架。它实际上是保护基于spring的应用程序的标准。
Spring Security是一个框架,侧重于为Java应用程序提供身份验证和授权。与所有Spring项目一样,Spring安全性的真正强大之处在于它可以轻松地扩展以满足定制需求
从官网的介绍中可以知道这是一个权限框架。想我们之前做项目是没有使用框架是怎么控制权限的?对于权限 一般会细分为功能权限,访问权限,和菜单权限。代码会写的非常的繁琐,冗余。
怎么解决之前写权限代码繁琐,冗余的问题,一些主流框架就应运而生而Spring Scecurity就是其中的一种。
Spring 是一个非常流行和成功的 Java 应用开发框架。Spring Security 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分。用户认证指的是验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。用户授权指的是验证某个用户是否有权限执行某个操作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。
对于上面提到的两种应用情景,Spring Security 框架都有很好的支持。在用户认证方面,Spring Security 框架支持主流的认证方式,包括 HTTP 基本认证、HTTP 表单验证、HTTP 摘要认证、OpenID 和 LDAP 等。在用户授权方面,Spring Security 提供了基于角色的访问控制和访问控制列表(Access Control List,ACL),可以对应用中的领域对象进行细粒度的控制。
实战测试
实验环境搭建
1、新建一个初始的springboot项目web模块,thymeleaf模块
2、导入静态资源
静态资源
welcome.html
|views
|level1
1.html
2.html
3.html
|level2
1.html
2.html
3.html
|level3
1.html
2.html
3.html
Login.html
3、controller跳转!
package top.saodisheng.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
/**
* Description:
*
* @author 扫地生_saodisheng
*/
@Controller
public class RouterController {
@RequestMapping({"/", "/index"})
public String index() {
return "index";
}
@RequestMapping("/toLogin")
public String toLogin() {
return "views/login";
}
@RequestMapping("/level1/{id}")
public String toLevel1(@PathVariable("id")int id) {
return "views/level1" + id;
}
@RequestMapping("/level2/{id}")
public String toLevel2(@PathVariable("id")int id) {
return "views/level2" + id;
}
@RequestMapping("/level3/{id}")
public String toLevel3(@PathVariable("id")int id) {
return "views/level3" + id;
}
}
4、测试实验环境是否OK!
认识SpringSecurity
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,它可以实现强大的Web安全控制,对于安全控制,我们仅需要引入 spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!
记住几个类:
- WebSecurityConfigurerAdapter:自定义Security策略
- AuthenticationManagerBuilder:自定义认证策略
- @EnableWebSecurity:开启WebSecurity模式
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication)
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
“授权” (Authorization)
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在Spring Security 中存在。
认证和授权
目前,我们的测试环境,是谁都可以访问的,我们使用 Spring Security 增加上认证和授权的功能
1、引入 Spring Security 模块
org.springframework.boot
spring-boot-starter-security
2、编写 Spring Security 配置类
参考官网:https://spring.io/projects/spring-security
查看我们自己项目中的版本,找到对应的帮助文档:
https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5 #servlet-applications 8.16.4
3、编写基础配置类
package top.saodisheng.config;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
/**
* Description:
*
* @author 扫地生_saodisheng
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
}
}
4、定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
// 首页所有人可以访问,但是功能页只有对应有权限的人才能访问
// 配置请求授权的规则
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
// 没有授权默认回到登录页, 需要开启登录的页面
http.formLogin();
}
5、测试一下:发现除了首页都进不去了!因为我们目前没有登录的角色,因为请求需要登录的角色拥有对应的权限才可以!
6、在configure()方法中加入以下配置,开启自动配置的登录功能!
// 开启自动配置的登录功能
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin();
7、测试一下:发现,没有权限的时候,会跳转到登录的页面!
8、查看刚才登录页的注释信息;
我们可以定义认证规则,重写configure(AuthenticationManagerBuilder auth)方法
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
auth.inMemoryAuthentication()
.withUser("saodisheng").password("123456").roles("vip2","vip3")
.and()
.withUser("root").password("123456").roles("vip1","vip2","vip3")
.and()
.withUser("guest").password("123456").roles("vip1","vip2");
}
9、测试,我们可以使用这些账号登录进行测试!发现会报错!
There is no PasswordEncoder mapped for the id “null”
10、原因,我们要将前端传过来的密码进行某种方式加密,否则就无法登录,修改代码
//定义认证规则
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder()) // 密码编码,不使用的话服务器可能会报错
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3")
.and()
.withUser("saodisheng").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2")
.and()
.withUser("laoliu").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
权限控制和注销
1、开启自动配置的注销的功能
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//....
//开启自动配置的注销的功能
// /logout 注销请求
http.logout();
}
2、我们在前端,增加一个注销的按钮,index.html 导航栏中
注销
3、我们可以去测试一下,登录成功后点击注销,发现注销完毕会跳转到登录页面!
4、但是,我们想让他注销成功后,依旧可以跳转到首页,该怎么处理呢?
// .logoutSuccessUrl("/"); 注销成功来到首页
http.logout().logoutSuccessUrl("/");
5、测试,注销完毕后,发现跳转到首页OK
6、我们现在又来一个需求:用户没有登录的时候,导航栏上只显示登录按钮,用户登录之后,导航栏可以显示登录的用户信息及注销按钮!还有就是,比如saodisheng这个用户,它只有 vip1,vip2功能,那么登录则只显示这两个功能,而vip1的功能菜单不显示!这个就是真实的网站情况了!该如何做呢?
我们需要结合thymeleaf中的一些功能
sec:authorize="isAuthenticated()":是否认证登录!来显示不同的页面
Maven依赖:
org.thymeleaf.extras
thymeleaf-extras-springsecurity5
3.0.4.RELEASE
7、修改我们的 前端页面
-
导入命名空间
-
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
-
修改导航栏,增加认证判断
8、重启测试,我们可以登录试试看,登录成功后确实,显示了我们想要的页面;
9、如果注销404了,就是因为它默认防止csrf跨站请求伪造,因为会产生安全问题,我们可以将请求改为post表单提交,或者在spring security中关闭csrf功能;我们试试:在 配置中增加 http.csrf().disable();
http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");
10、我们继续将下面的角色功能块认证完成!
11、测试一下!
12、权限控制和注销搞定!
记住我
现在的情况,我们只要登录之后,关闭浏览器,再登录,就会让我们重新登录,但是很多网站的情况,就是有一个记住密码的功能,这个该如何实现呢?很简单
1、开启记住我功能
//定制请求的授权规则
@Override
protected void configure(HttpSecurity http) throws Exception {
//......
//记住我
http.rememberMe();
}
2、我们再次启动项目测试一下,发现登录页多了一个记住我功能,我们登录之后关闭 浏览器,然后重新打开浏览器访问,发现用户依旧存在!
思考:如何实现的呢?其实非常简单
我们可以查看浏览器的cookie
3、我们点击注销的时候,可以发现,spring security 帮我们自动删除了这个 cookie
定制登录页
现在这个登录页面都是spring security 默认的,怎么样可以使用我们自己写的Login界面呢?
1、在刚才的登录页配置后面指定 loginpage
http.formLogin().loginPage("/toLogin");
2、然后前端也需要指向我们自己定义的 login请求
登录
3、我们登录,需要将这些信息发送到哪里,我们也需要配置,login.html 配置提交请求及方式,方式必须为post:
在 loginPage()源码中的注释上有写明:
4、这个请求提交上来,我们还需要验证处理,怎么做呢?我们可以查看formLogin()方法的源码!我们配置接收登录的用户名和密码的参数!
http.formLogin()
.usernameParameter("use") // 指定登录页面的用户名框的名称
.passwordParameter("pwd") // 指定登录界面的密码框的名称
.loginPage("/toLogin") // 指定登录页面
.loginProcessingUrl("/login"); // 登陆表单提交请求
5、在登录页增加记住我的多选框
记住我
6、后端验证处理!
//定制记住我的参数!
http.rememberMe().rememberMeParameter("remember");
7、测试,OK
完整配置代码
package top.saodisheng.config;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
/**
* Description:
*
* @author 扫地生_saodisheng
*/
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
// 首页所有人可以访问,但是功能页只有对应有权限的人才能访问
// 配置请求授权的规则
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
//开启自动配置的登录功能:如果没有权限,就会跳转到登录页面!
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http.formLogin()
.usernameParameter("use") // 指定登录页面的用户名框的名称
.passwordParameter("pwd") // 指定登录界面的密码框的名称
.loginPage("/toLogin") // 指定登录页面
.loginProcessingUrl("/login"); // 登陆表单提交请求
//开启自动配置的注销的功能
// /logout 注销请求
// .logoutSuccessUrl("/"); 注销成功来到首页
http.csrf().disable();//关闭csrf功能:跨站请求伪造,默认只能通过post方式提交logout请求
http.logout().logoutSuccessUrl("/");
// 开启记住我的功能,cookie,默认保存两周,可以自定义接受前端参数
http.rememberMe().rememberMeParameter("remember");
}
// 认证,Springboot 2.1.x可以直接使用
// 密码编码:PasswordEncoder
// 在Spring Security 5.0+ 新增了很多加密方法,如果为使用加密方法服务器会报错(防止被反编译)
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,也可以在jdbc中去拿....
//Spring security 5.0中新增了多种加密方式,也改变了密码的格式。
//要想我们的项目还能够正常登陆,需要修改一下configure中的代码。我们要将前端传过来的密码进行某种方式加密
//spring security 官方推荐的是使用bcrypt加密方式。
auth.inMemoryAuthentication()
.passwordEncoder(new BCryptPasswordEncoder()) // 密码编码,不使用的话服务器可能会报错
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2", "vip3")
.and()
.withUser("saodisheng").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1", "vip2")
.and()
.withUser("laoliu").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
}
13、Shiro
1、Shiro简介
1.1、什么是Shiro?
- Apache Shiro 是一个Java 的安全(权限)框架。
- Shiro 可以非常容易的开发出足够好的应用,其不仅可以用在JavaSE环境,也可以用在JavaEE环境。
- Shiro可以完成,认证,授权,加密,会话管理,Web集成,缓存等。
- 下载地址:http://shiro.apac he.org/
1.2、有哪些功能?
- Authentication: 身 份 认 证 、 登 录 , 验 证 用 户 是 不 是 拥 有 相 应 的 身 份;
- Authorization:授权,即权限验证,验证某个已认证的用户是否拥有某个权限,即判断用户能否进行什么操作,如:验证某个用户是否拥有某个角色,或者细粒度的验证某个用户对某个资源是否具有某个权限!
- Session Manager:会话管理,即用户登录后就是第一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通的JavaSE环境,也可以是Web环境;
- Cryptography:加密,保护数据的安全性,如密码加密存储到数据库中,而不是明文存储;
- Web Support:Web支持,可以非常容易的集成到Web环境;
- Caching:缓存,比如用户登录后,其用户信息,拥有的角色、权限不必每次去查,这样可以提高效率
- Concurrency:Shiro支持多线程应用的并发验证,即,如在一个线程中开启另一个线程,能把权限自动的传播过去
- Testing:提供测试支持;
- Run As:允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
- Remember Me:记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了
1.3、Shiro架构(外部)
从外部来看Shiro,即从应用程序角度来观察如何使用shiro完成工作:
- subject: 应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject,Subject代表了当前的用户,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是Subject,如网络爬虫,机器人等,与Subject的所有交互都会委托给SecurityManager;Subject其实是一个门面,SecurityManageer 才是实际的执行者
- SecurityManager:安全管理器,即所有与安全有关的操作都会与SercurityManager交互,并且它管理着所有的Subject,可以看出它是Shiro的核心,它负责与Shiro的其他组件进行交互,它相当于SpringMVC的DispatcherServlet的角色
- Realm:Shiro从Realm获取安全数据(如用户,角色,权限),就是说SecurityManager 要验证
- 用户身份,那么它需要从Realm 获取相应的用户进行比较,来确定用户的身份是否合法;也需要从
- Realm得到用户相应的角色、权限,进行验证用户的操作是否能够进行,可以把Realm看成DataSource;
1.4、Shiro架构(内部)
- Subject:任何可以与应用交互的 ‘用户’;
- Security Manager:相当于SpringMVC中的DispatcherServlet;是Shiro的心脏,所有具体的交互都通过Security Manager进行控制,它管理者所有的Subject,且负责进行认证,授权,会话,及缓存的管理。
- Authenticator:负责Subject认证,是一个扩展点,可以自定义实现;可以使用认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
- Authorizer:授权器,即访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的那些功能;
- Realm:可以有一个或者多个的realm,可以认为是安全实体数据源,即用于获取安全实体的,可以用JDBC实现,也可以是内存实现等等,由用户提供;所以一般在应用中都需要实现自己的realm
- SessionManager:管理Session生命周期的组件,而Shiro并不仅仅可以用在Web环境,也可以用在普通的JavaSE环境中
- CacheManager:缓存控制器,来管理如用户,角色,权限等缓存的;因为这些数据基本上很少改变,放到缓存中后可以提高访问的性能;
- Cryptography:密码模块,Shiro 提高了一些常见的加密组件用于密码加密,解密等
2、HellowWorld
2.1、快速实战
查看官网文档:http://shiro.apache.org/tutorial.html
官方的quickstart:https://github.com/apache/shiro/tree/master/samples/quickstart/
- 创建一个maven父工程,用于学习Shiro,删掉不必要的东西
- 创建一个普通的Maven子工程:shiro-01-helloworld
- 根据官方文档,我们来导入Shiro的依赖
org.apache.shiro
shiro-core
1.4.1
org.slf4j
slf4j-simple
1.7.21
test
org.slf4j
jcl-over-slf4j
1.7.21
test
-
编写Shrio配置
log4j.properties
log4j.rootLogger=INFO, stdout log4j.appender.stdout=org.apache.log4j.ConsoleAppender log4j.appender.stdout.layout=org.apache.log4j.PatternLayout log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m %n # General Apache libraries log4j.logger.org.apache=WARN # Spring log4j.logger.org.springframework=WARN # Default Shiro logging log4j.logger.org.apache.shiro=INFO # Disable verbose logging log4j.logger.org.apache.shiro.util.ThreadContext=WARN log4j.logger.org.apache.shiro.cache.ehcache.EhCache=WARN
shiro.ini(IDEA需要安装插件:https://blog.csdn.net/cxl0525/article/details/104647375/)
# ---------------------------------------------------------------------- ------- # Users and their assigned roles # # Each line conforms to the format defined in the 1234 # org.apache.shiro.realm.text.TextConfigurationRealm#setUserDefinitions JavaDoc # ---------------------------------------------------------------------- ------- [users] # user 'root' with password 'secret' and the 'admin' role root = secret, admin # user 'guest' with the password 'guest' and the 'guest' role guest = guest, guest # user 'presidentskroob' with password '12345' ("That's the same combination on # my luggage!!!" ;)), and role 'president' presidentskroob = 12345, president # user 'darkhelmet' with password 'ludicrousspeed' and roles 'darklord' and 'schwartz' darkhelmet = ludicrousspeed, darklord, schwartz # user 'lonestarr' with password 'vespa' and roles 'goodguy' and 'schwartz' lonestarr = vespa, goodguy, schwartz # ---------------------------------------------------------------------- ------- # Roles with assigned permissions # # Each line conforms to the format defined in the #org.apache.shiro.realm.text.TextConfigurationRealm#setRoleDefinitions JavaDoc # ---------------------------------------------------------------------- ------- [roles] # 'admin' role has all permissions, indicated by the wildcard '*' admin = * # The 'schwartz' role can do anything (*) with any lightsaber: schwartz = lightsaber:* # The 'goodguy' role is allowed to 'drive' (action) the winnebago (type) with # license plate 'eagle5' (instance specific id) goodguy = winnebago:drive:eagle5
-
编写我们的QuickStrat
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Simple Quickstart application showing how to use Shiro's API. */ public class Quickstart { private static final transient Logger log =LoggerFactory.getLogger(Quickstart.class); public static void main(String[] args) { // The easiest way to create a Shiro SecurityManager with configured // realms, users, roles and permissions is to use the simple INI config. // We'll do that by using a factory that can ingest a .ini file and // return a SecurityManager instance: // Use the shiro.ini file at the root of the classpath // (file: and url: prefixes load from files and urls respectively): Factory
factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); // for this simple example quickstart, make the SecurityManager // accessible as a JVM singleton. Most applications wouldn't do this // and instead rely on their container configuration or web.xml for // webapps. That is outside the scope of this simple quickstart, so // we'll just do the bare minimum so you can continue to get a feel // for things. SecurityUtils.setSecurityManager(securityManager); // Now that a simple Shiro environment is set up, let's see what you can do: // get the currently executing user: Subject currentUser = SecurityUtils.getSubject(); // Do some stuff with a Session (no need for a web or EJB container!!!) Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("Retrieved the correct value! [" + value + "]"); } // let's login the current user so we can check against roles and permissions: if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); try { currentUser.login(token); } catch (UnknownAccountException uae) { log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // ... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { //unexpected condition? error? } } //say who they are: //print their identifying principal (in this case, a username): log.info("User [" + currentUser.getPrincipal() + "] logged in successfully."); //test a role: if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } //test a typed permission (not instance-level) if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } //a (very powerful) Instance Level permission: if (currentUser.isPermitted("winnebago:drive:eagle5")) { log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " + "Here are the keys - have fun!"); } else { log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!"); } //all done - log out! currentUser.logout(); System.exit(0); } } -
测试一下
-
报错,则导入commons-logging的依赖
commons-logging commons-logging 1.2 -
发现,执行完毕什么都没有,可能是maven依赖中的作用域问题,我们需要将scope作用域删掉,默认是在test,然后重启,那么我们的quickstart就结束了,默认的日志消息!
[main] INFO org.apache.shiro.session.mgt.AbstractValidatingSessionManager - Enabling session validation scheduler... [main] INFO Quickstart - Retrieved the correct value! [aValue] [main] INFO Quickstart - User [lonestarr] logged in successfully. [main] INFO Quickstart - May the Schwartz be with you! [main] INFO Quickstart - You may use a lightsaber ring. Use it wisely. [main] INFO Quickstart - You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. Here are the keys - have fun!
2.2、阅读代码
-
导入一堆包
-
类的描述
/** * Simple Quickstart application showing how to use Shiro's API. * 简单的快速启动应用程序,演示如何使用Shiro的API。 */
-
通过工厂模式创建SecurityManager的实例对象
// realms, users, roles and permissions is to use the simple INI config. // We'll do that by using a factory that can ingest a .ini file and // return a SecurityManager instance: // 使用类路径根目录下的shiro.ini文件 // Use the shiro.ini file at the root of the classpath // (file: and url: prefixes load from files and urls respectively): Factory
factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); // for this simple example quickstart, make the SecurityManager // accessible as a JVM singleton. Most applications wouldn't do this // and instead rely on their container configuration or web.xml for // webapps. That is outside the scope of this simple quickstart, so // we'll just do the bare minimum so you can continue to get a feel // for things. SecurityUtils.setSecurityManager(securityManager); // 现在已经建立了一个简单的Shiro环境,让我们看看您可以做什么: // Now that a simple Shiro environment is set up, let's see what you can do: -
获取当前的Subject
// get the currently executing user: 获取当前正在执行的用户 Subject currentUser = SecurityUtils.getSubject();
-
session的操作
// 用会话做一些事情(不需要web或EJB容器!!!) // Do some stuff with a Session (no need for a web or EJB container!!!) Session session = currentUser.getSession(); //获得session session.setAttribute("someKey", "aValue"); //设置Session的值! String value = (String) session.getAttribute("someKey"); //从session中获取值 if (value.equals("aValue")) { //判断session中是否存在这个值! log.info("==Retrieved the correct value! [" + value + "]"); }
-
用户认证功能
// 测试当前的用户是否已经被认证,即是否已经登录! // let's login the current user so we can check against roles and permissions: if (!currentUser.isAuthenticated()) { // isAuthenticated();是否认证 //将用户名和密码封装为 UsernamePasswordToken ; UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); //记住我功能 try { currentUser.login(token); //执行登录,可以登录成功的! } catch (UnknownAccountException use) { //如果没有指定的用户,则 UnknownAccountException异常 log.info("There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { //密码不对的异常! log.info("Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { //用户被锁定的异常 log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); }// ... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { //认证异常,上面的异常都是它的子类 //unexpected condition? error? } } //说出他们是谁: //say who they are: //打印他们的标识主体(在本例中为用户名): //print their identifying principal (in this case, a username): log.info("User [" + currentUser.getPrincipal() + "] logged in successfully.");
-
角色检查
//test a role: //是否存在某一个角色 if (currentUser.hasRole("schwartz")) { log.info("May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); }
-
权限检查,粗粒度
//测试用户是否具有某一个权限,行为 //test a typed permission (not instance-level) if (currentUser.isPermitted("lightsaber:wield")) { log.info("You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); }
-
权限检查,细粒度
//测试用户是否具有某一个权限,行为,比上面更加的具体! //a (very powerful) Instance Level permission: if (currentUser.isPermitted("winnebago:drive:eagle5")) { log.info("You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " + "Here are the keys - have fun!"); } else { log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!"); }
-
注销操作
//执行注销操作! //all done - log out! currentUser.logout();
-
退出系统 System.exit(0);
3、SpringBoot集成
3.1、准备工作
-
搭建一个SpringBoot项目,选中web模块即可
-
导入Maven依赖thymeleaf
org.thymeleaf thymeleaf-spring5 org.thymeleaf.extras thymeleaf-extras-java8time -
编写一个页面index.html (templates)
Title 首页
-
编写controller进行访问调试
package top.saodisheng.controller; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; /** * Description: * * @author 扫地生_saodisheng */ @Controller public class MyController { @RequestMapping({"/", "/index"}) public String toIndex(Model model) { model.addAttribute("msg", "hello, Shiro"); return "index"; } }
-
测试访问首页
3.2、整合Shiro
回顾核心API:
-
Subject:用户主体
-
SecurityManager:安全管理器
-
Realm:Shiro 连接数据
步骤:
-
导入Shiro和Spring整合依赖
org.apache.shiro shiro-spring 1.4.1 -
编写Shiro配置类(config包)
package top.saodisheng.config; import org.springframework.context.annotation.Configuration; /** * Description: * Shiro配置类 * @author 扫地生_saodisheng */ @Configuration public class ShiroConfig { // 创建 ShiroFilterFactoryBean // 创建 DefaultWebSecurityManager // 创建 realm对象 }
-
我们倒着来,先想办法创建一个realm对象
-
我们需要自定义一个realm的类,用来编写一些查询的方法,或者认证与授权的逻辑
package com.kuang.config; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.subject.PrincipalCollection; //自定义Realm public class UserRealm extends AuthorizingRealm { //执行授权逻辑 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("执行了=>授权逻辑PrincipalCollection"); return null; } //执行认证逻辑 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了=>认证逻辑AuthenticationToken"); return null; } }
-
将这个类注册到我们的bean中 (shiroConfig)
@Configuration public class ShiroConfig { //创建 ShiroFilterFactoryBean //创建 DefaultWebSecurityManager //创建 realm 对象 @Bean public UserRealm userRealm(){ return new UserRealm(); } }
-
接下来我们去创建DefaultWebSecurityManager了
//创建 DefaultWebSecurityManager @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm")UserRealm userRealm){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //关联Realm securityManager.setRealm(userRealm); return securityManager; }
-
接下来我们该去创建ShiroFilterFatoryBean了
//创建 ShiroFilterFactoryBean @Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurity Manager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //设置安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); return shiroFilterFactoryBean; }
完整的配置如下:
package com.kuang.config;
import org.springframework.context.annotation.Configuration;
//声明为配置类
@Configuration
public class ShiroConfig {
//创建 ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityMan ager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
//设置安全管理器
shiroFilterFactoryBean.setSecurityManager(securityManager);
return shiroFilterFactoryBean;
}
//创建 DefaultWebSecurityManager
@Bean(name = "securityManager")
public DefaultWebSecurityManager getDefaultWebSecurityManager(@Qualifier("userRealm")UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
//关联Realm
securityManager.setRealm(userRealm);
return securityManager;
}
//创建 realm 对象
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
}
3.3、页面拦截器实现
-
编写两个页面、在templates目录下新建一个user目录 add.html update.html
add
update
-
编写跳转到页面的controller
@RequestMapping("/user/add") public String toAdd(){ return "user/add"; } @RequestMapping("/user/update") public String toUpdate(){ return "user/update"; }
-
在ndex页面上,增加跳转链接
add | update
-
测试页面跳转是否ok
-
准备添加Shiro的内置过滤器
@Bean public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("securityManager")DefaultWebSecurityManager securityManager){ ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //设置安全管理器 shiroFilterFactoryBean.setSecurityManager(securityManager); /* 添加Shiro内置过滤器,常用的有如下过滤器: anon: 无需认证就可以访问 authc: 必须认证才可以访问 user: 如果使用了记住我功能就可以直接访问 perms: 拥有某个资源权限才可以访问 role: 拥有某个角色权限才可以访问 */ Map
filterMap = new LinkedHashMap (); filterMap.put("/user/add","authc"); filterMap.put("/user/update","authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); return shiroFilterFactoryBean; } -
再起启动测试,访问链接进行测试!拦截OK!但是发现,点击后会跳转到一个Login.jsp页面,这个不是我们想要的效果,我们需要自己定义一个Login页面!
-
我们编写一个自己的Login页面
登录页面 登录页面
-
编写跳转的controller
@RequestMapping("/toLogin") public String toLogin(){ return "login"; }
-
在shiro中配置一下 ShiroFilterFactoryBean()方法下面
//修改到要跳转的login页面; shiroFilterFactoryBean.setLoginUrl("/toLogin");
-
再次测试,成功跳转到我们指定的Login页面
-
优化一下代码,我们这里的拦截器可以使用通配符来操作
Map
filterMap = new LinkedHashMap (); //filterMap.put("/user/add","authc"); //filterMap.put("/user/update","authc"); filterMap.put("/user/*","authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterMap); -
测试完成
3.4、登录认证操作的实现
-
编写一个登录的controller
//登录操作 @RequestMapping("/login") public String login(String username,String password,Model model){ //使用shiro,编写认证操作 //1. 获取Subject Subject subject = SecurityUtils.getSubject(); //2. 封装用户的数据 UsernamePasswordToken token = new UsernamePasswordToken(username, password); //3. 执行登录的方法,只要没有异常就代表登录成功! try { subject.login(token); //登录成功!返回首页 return "index"; } catch (UnknownAccountException e) { //用户名不存在 model.addAttribute("msg","用户名不存在"); return "login"; } catch (IncorrectCredentialsException e) { //密码错误 model.addAttribute("msg","密码错误"); return "login"; } }
-
在前端修改对应的信息输出或者请求
登录页面增加一个msg提示
给表单增加一个提交地址
-
理论,假设我们提交了表单,它会经过我们刚才编写的UserRealm,我们提交测试一下
确实执行了我们的认证逻辑
-
在UserRealm中编写用户的判断逻辑
//执行认证逻辑 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了=>认证逻辑AuthenticationToken"); //假设数据库的用户名和密码 String name = "root"; String password = "123456"; //1.判断用户名 UsernamePasswordToken userToken = (UsernamePasswordToken)token; if (!userToken.getUsername().equals(name)){ //用户名不存在 return null; //shiro底层就会抛出 UnknownAccountException } //2. 验证密码,我们可以使用一个AuthenticationInfo实现类 SimpleAuthenticationInfo // shiro会自动帮我们验证!重点是第二个参数就是要验证的密码! return new SimpleAuthenticationInfo("", password, ""); }
-
测试一下,成功实现登录的认证操作
3.5、整合数据库
-
导入Mybatis相关依赖
org.mybatis.spring.boot mybatis-spring-boot-starter 2.1.0 mysql mysql-connector-java runtime log4j log4j 1.2.17 com.alibaba druid 1.1.12 -
编写配置文件链接配置 application.yaml
spring: datasource: username: root password: 123456 #?serverTimezone=UTC解决时区的报错 url: jdbc:mysql://localhost:3306/mybatis? serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8 driver-class-name: com.mysql.jdbc.Driver type: com.alibaba.druid.pool.DruidDataSource #Spring Boot 默认是不注入这些属性值的,需要自己绑定 #druid 数据源专有配置 initialSize: 5 minIdle: 5 maxActive: 20 maxWait: 60000 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: SELECT 1 FROM DUAL testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true #配置监控统计拦截的filters,stat:监控统计、log4j:日志记录、wall:防御sql注入 #如果允许时报错 java.lang.ClassNotFoundException: org.apache.log4j.Priority #则导入 log4j 依赖即可,Maven 地址: https://mvnrepository.com/artifact/log4j/log4j filters: stat,wall,log4j maxPoolPreparedStatementPerConnectionSize: 20 useGlobalDataSourceStat: true connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
-
编写mybatis的配置applilcation.properties
#别名配置 mybatis.type-aliases-package=top.saodisheng.pojo mybatis.mapper-locations=classpath:mapper/*.xml
-
编写实体类,引入Lombok
org.projectlombok lombok 1.16.10 package top.saodisheng.pojo; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * Description: * 实体类 * @author 扫地生_saodisheng */ @Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; // 编号 private String name; // 姓名 private String pwd; // 密码 }
-
编写Mapper接口
@Repository @Mapper public interface UserMapper { public User queryUserByName(String name); }
-
编写Mapper配置文件
-
编写UserService层
public interface UserService { public User queryUserByName(String name); }
package top.saodisheng.service; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import top.saodisheng.mapper.UserMapper; import top.saodisheng.pojo.User; /** * Description: * * @author 扫地生_saodisheng */ @Service public class UserServiceImpl implements UserService { @Autowired UserMapper userMapper; @Override public User queryUserByName(String name) { return userMapper.queryUserByName(name); } }
-
测试
class Shiro02SpringbootApplicationTests { @Autowired UserServiceImpl userService; @Test void contextLoads() { User user = userService.queryUserByName("root"); System.out.println(user); } }
-
改造UserRealm,连接到数据库进行真实的操作
//自定义Realm public class UserRealm extends AuthorizingRealm { @Autowired UserService userService; //执行授权逻辑 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("执行了=>授权逻辑PrincipalCollection"); return null; } //执行认证逻辑 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("执行了=>认证逻辑AuthenticationToken"); UsernamePasswordToken userToken = (UsernamePasswordToken)token; //真实连接数据库 User user = userService.queryUserByName(userToken.getUsername()); if (user==null){ //用户名不存在 return null; //shiro底层就会抛出 UnknownAccountException } return new SimpleAuthenticationInfo("", user.getPwd(), ""); } }
-
测试
3.5、思考:密码比对原理探究
思考?这个Shiro,是怎么帮我们实现密码自动比对的呢?
我们可以去 realm的父类 AuthorizingRealm 的父类 AuthenticatingRealm 中找一个方法
核心: getCredentialsMatcher() 翻译过来:获取证书匹配器
我们去看这个接口 CredentialsMatcher 有很多的实现类,MD5盐值加密
我们的密码一般都不能使用明文保存?需要加密处理;思路分析
-
如何把一个字符串加密为MD5
-
替换当前的Realm 的 CredentialsMatcher 属性,直接使用 Md5CredentialsMatcher 对象,并设置加密算法
3.6、用户授权操作
使用shiro的过滤器来拦截请求即可
-
在 ShiroFilterFactoryBean 中添加一个过滤器
//授权过滤器 filterMap.put("/user/add","perms[user:add]"); //大家记得注意顺序!
-
我们再次启动测试一下,访问add,发现以下错误!未授权错误!
-
注意:当我们实现权限拦截后,shiro会自动跳转到未授权的页面,但我们没有这个页面,所有401了
-
配置一个未授权的提示的页面,增加一个controller提示
@RequestMapping("/noauth") @ResponseBody public String noAuth(){ return "未经授权不能访问此页面"; }
然后再 shiroFilterFactoryBean 中配置一个未授权的请求页面!
shiroFilterFactoryBean.setUnauthorizedUrl("/noauth");
-
测试,现在有了授权可以跳转到我们指定的位置了
3.7、Shiro授权
在UserRealm 中添加授权的逻辑,增加授权的字符串!
//执行授权逻辑
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
System.out.println("执行了=>授权逻辑PrincipalCollection");
//给资源进行授权
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
//添加资源的授权字符串
info.addStringPermission("user:add");
return info;
}
我们再次登录测试,发现登录的用户是可以进行访问add 页面了!授权成功!
问题,我们现在完全是硬编码,无论是谁登录上来,都可以实现授权通过,但是真实的业务情况应该是,每个用户拥有自己的一些权限,从而进行操作,所以说,权限,应该在用户的数据库中,正常的情况下,应该数据库中是由一个权限表的,我们需要联表查询,但是这里为了大家操作理解方便一些,我们直接在数据库表中增加一个字段来进行操作!
-
修改实体类,增加一个字段
@Data @AllArgsConstructor @NoArgsConstructor public class User { private int id; private String name; private String pwd; private String perms; }
-
我们现在需要再自定义的授权认证中,获取登录的用户,从而实现动态认证授权操作!
-
在用户登录授权的时候,将用户放在 Principal 中,改造下之前的代码
return new SimpleAuthenticationInfo(user, user.getPwd(), "");
-
然后在授权的地方获得这个用户,从而获得它的权限
//执行授权逻辑 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("执行了=>授权逻辑PrincipalCollection"); //给资源进行授权 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); //添加资源的授权字符串 //info.addStringPermission("user:add"); Subject subject = SecurityUtils.getSubject(); //获得当前对象 User currentUser = (User) subject.getPrincipal(); //拿到User对象 info.addStringPermission(currentUser.getPerms()); //设置权限 return info; }
-
-
我们给数据库中的用户增加一些权限
-
在过滤器中,将 update 请求也进行权限拦截下
//授权过滤器 filterMap.put("/user/add","perms[user:add]"); filterMap.put("/user/update","perms[user:update]");
-
我们启动项目,登录不同的账户,进行测试一下!
-
测试完美通过OK!
3.8、整合Thymeleaf
根据权限展示不同的前端页面
-
添加Maven的依赖;
com.github.theborakompanioni thymeleaf-extras-shiro 2.0.0 -
配置一个shiro的Dialect ,在shiro的配置中增加一个Bean
//配置ShiroDialect:方言,用于 thymeleaf 和 shiro 标签配合使用 @Bean public ShiroDialect getShiroDialect(){ return new ShiroDialect(); }
-
修改前端的配置,需要加上命名空间:xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro"
-
我们在去测试一下,可以发现,现在首页什么都没有了,因为我们没有登录,我们可以尝试登录下,来判断这个Shiro的效果!登录后,可以看到不同的用户,有不同的效果,现在就已经接近完美了~!还不是最完美
-
为了完美,我们在用户登录后应该把信息放到Session中,我们完善下!在执行认证逻辑时候,加入session
Subject subject = SecurityUtils.getSubject(); subject.getSession().setAttribute("loginUser",user);
-
前端从session中获取,然后用来判断是否显示登录
-
测试,效果完美~