封面画师:adsuger 封面ID:74171912
1. SpringSecurity
在Web开发中,安全一直都是一个非常重要的方面,如果开发的应用出现安全漏洞,极大概率会造成隐私泄露。虽然它属于应用的非功能性需求,但我们应该在应用设计初期就将其考虑进来,如果不在设计之初就考虑清楚,架构一旦确定再进行更改会对架构产生较大的改动。虽然使用过滤器和拦截器也可以达到安全处理,但市面上也有集成的安全框架。
在市面上有两种比较出名的安全框架,一种是SpringSecurity,一种是Shiro,它们主要用来做两件事:认证、授权。
权限分类:功能权限、访问权限、菜单权限,我们可以使用大量的原生代码编写拦截器或过滤器来实现这些权限。
我们先对SpringSecurity进行简单的介绍。
先来一手官网:SpringSecurity官网 😝
1.1 环境搭建
创建项目时,勾选web与thymeleaf,向项目中添加web与thymeleaf的启动器 。
导入静态资源(目录结构如下):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 index.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
为了便于测试,我们可以关闭模板引擎的缓存:
1 spring.thymeleaf.cache=false
编写Controller,实现基本页面跳转,也是相当于测试:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 @ Controller
public class RouterController {
@ RequestMapping ({ "/" , "/index" })
public String index (){
return "index" ;
}
@ RequestMapping ( "/toLogin" )
public String toLogin (){
return "views/login" ;
}
@ 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;
}
}
编写好代码后,我们可以运行项目、测试环境!
1.2 初识SpringSecurity
PS:回忆并比较我们学习过的Spring AOP:横切与配置类。
Spring Security 是针对Spring项目的安全框架,也是Spring Boot底层安全模块默认的技术选型,它可以实现强大的Web安全控制,对于安全控制,我们仅需要引入spring-boot-starter-security 模块,进行少量的配置,即可实现强大的安全管理!
记住我们常用的几个类:
WebSecurityConfigurerAdapter:自定义Security策略
AuthenticationManagerBuilder:自定义认证策略
@EnableWebSecurity:开启WebSecurity模式
Spring Security的两个主要目标是 “认证” 和 “授权”(访问控制)。
“认证”(Authentication):
身份验证是关于验证您的凭据,如用户名/用户ID和密码,以验证您的身份。
身份验证通常通过用户名和密码完成,有时与身份验证因素结合使用。
“授权” (Authorization):
授权发生在系统成功验证您的身份后,最终会授予您访问资源(如信息,文件,数据库,资金,位置,几乎任何内容)的完全权限。
这个概念是通用的,而不是只在Spring Security 中存在。
1.3 认证和授权
导入SpringSecurity的启动器:
1 2 3 4 5 <!--security-->
< dependency >
< groupId >org.springframework.boot</ groupId >
< artifactId >spring-boot-starter-security</ artifactId >
</ dependency >
根据官方文档,编写 SpringSecurity 配置类,配置类固定的结构:
1 2 3 4 5 6 7 8 9 @ EnableWebSecurity // 开启WebSecurity模式
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 授权
@ Override
protected void configure ( HttpSecurity http ) throws Exception {
super . configure (http);
}
}
在配置类中设计权限规则:
1 2 3 4 5 6 7 8 9 10 //授权
@ Override
protected void configure ( HttpSecurity http) throws Exception {
//首页所有页可以访问,但是功能页需要访问权限
http . authorizeRequests ()
. antMatchers ( "/" ). permitAll ()
. antMatchers ( "/level1/**" ). hasAnyRole ( "vip1" )
. antMatchers ( "/level2/**" ). hasAnyRole ( "vip2" )
. antMatchers ( "/level3/**" ). hasAnyRole ( "vip3" );
}
然后,我们可以进行简单的测试。启动程序,进入首页,点击超链接,我们发现除了首页,其他界面都无法进入。报 403 状态,Access Denied。
因为我们没有进行登录,所以当我们进行需要登录的操作时,应该将页面跳转至登录界面,而不是报错。开启登录界面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 //授权
@ Override
protected void configure ( HttpSecurity http) throws Exception {
//首页所有页可以访问,但是功能页需要访问权限
http . authorizeRequests ()
. antMatchers ( "/" ). permitAll ()
. antMatchers ( "/level1/**" ). hasAnyRole ( "vip1" )
. antMatchers ( "/level2/**" ). hasAnyRole ( "vip2" )
. antMatchers ( "/level3/**" ). hasAnyRole ( "vip3" );
// 开启自动配置的登录功能
// /login 请求来到登录页
// /login?error 重定向到这里表示登录失败
http . formLogin ();
}
然后我们可以进行测试:没有登录但进行相关操作时,跳转至登录界面。
但是我们要怎么为用户设置权限呢?我们可以定义认证规则,重写configure()方法。SpringSecurity提供了两种方式,一种是在内存中定义,一种是从数据库取得。我们在此演示在内存中定义。
在配置类中定义认证规则:
1 2 3 4 5 6 7 8 9 10 11 12 13 //认证
//密码编码:PasswordEncoder
//在Spring Security 5.x中新增了多种加密方式,也改变了代码的书写方式
@ Override
protected void configure ( AuthenticationManagerBuilder auth) throws Exception {
//在内存中定义,真实场景从数据库读取
auth . inMemoryAuthentication (). passwordEncoder ( new BCryptPasswordEncoder ())
. withUser ( "yang" ). password ( new BCryptPasswordEncoder (). encode ( "123456" )). roles ( "vip2" , "vip3" )
. and ()
. withUser ( "root" ). password ( new BCryptPasswordEncoder (). encode ( "123456" )). roles ( "vip1" , "vip2" , "vip3" )
. and ()
. withUser ( "guest" ). password ( new BCryptPasswordEncoder (). encode ( "123456" )). roles ( "vip1" );
}
最后,进行测试!发现登陆成功,每个角色拥有自己的访问权限,且只能进行自己权限下的操作。😉
1.4 注销及权限控制
注销
开启注销功能:
1 2 3 4 5 6 7 //授权
@ Override
protected void configure ( HttpSecurity http) throws Exception {
//....
//开启自动配置的注销的功能
http . logout ();
}
添加注销超链接:
1 2 3 < a class = "item" th:href = "@{/logout}" >
< i class = "address card icon" ></ i > 注销
</ a >
然后进行测试:登陆成功后点击注销,跳转至登录页面。
但是,如果我们想要注销后跳转至首页,应该怎么做呢?
1 2 // 在注销功能后添加一个方法即可
http . logout (). logoutSuccessUrl ( "/" );
然后,我们再进行 测试,发现符合我们的预期。
权限控制
现在导航条上有两个按钮(或者说超链接),但是这显然不符合实际情况。我们需要在用户没有登录的时候,导航栏上只显示登录按钮;用户登录成功后,显示用户名、用户角色和注销按钮,同时页面中只显示用户拥有相信权限的内容。
因为我们使用的Thymeleaf模板引擎,因此我们需要结合Thymeleaf中的一些功能。
导入相关依赖:
1 2 3 4 5 6 <!-- thymeleaf-extras- springsecurity4 -->
< dependency >
< groupId >org.thymeleaf.extras</ groupId >
< artifactId >thymeleaf-extras-springsecurity4</ artifactId >
< version >3.0.4.RELEASE</ version >
</ dependency >
修改前端界面:
首先导入命名空间:
1 2 < html lang = "en" xmlns:th = "http://www.thymeleaf.org"
xmlns:sec = "http://www.thymeleaf.org/thymeleaf-extras-springsecurity4" >
修改导航栏,添加认证判断:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <!--登录注销-->
< div class = "right menu" >
<!--如果未登录-->
< 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" >
用户名:< span sec:authentication = "name" ></ span >
<!-- 角色:<span sec:authentication="principal.authorities"></span>-->
</ a >
</ div >
< div sec:authorize = "isAuthenticated()" >
< a class = "item" th:href = "@{/logout}" >
< i class = "sign-out icon" ></ i > 注销
</ a >
</ div >
</ div >
PS:在这里我们用到了sec:authorize="isAuthenticated()",用来验证是否登录,进而显示不同的界面。
然后我们重启项目,登录系统,查看是否修改成功。
如果我们登录后进行注销产生了 404 错误,因为Security默认防止 CSRF(跨站请求伪造)。Security认为会产生安全问题,因此产生 404 错误,我们可以将请求改为post表单提交,或者在Spring Security中关闭 CSRF功能以消除 404 错误。
我们可以在配置类中增加 http.csrf().disable();以关闭 Security的防止CSRF功能。
1 2 3 4 5 6 7 8 9 10 11 12 @ Override
protected void configure ( HttpSecurity http) throws Exception {
// ...
//防止网站攻击
http . csrf (). disable (); //关闭csrf
//开启注销,并跳转至首页
http . logout (). logoutSuccessUrl ( "/" );
}
我们还没有写完,因为还没有实现根据用户角色实现界面显示对应的内容。继续修改界面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 < div >
< br >
< div class = "ui three column stackable grid" >
<!--内容用户权限动态显示-->
< div class = "column" sec:authorize = "hasRole('vip1')" >
< 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 class = "column" sec:authorize = "hasRole('vip2')" >
< 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 class = "column" sec:authorize = "hasRole('vip3')" >
< 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 >
然后,我们就可以进行测试验证功能是否实现了。
1.5 记住我及首页定制
记住我
所谓“记住我”,就是用户登录后关掉浏览器,再次打开浏览器时不用再次登录。
直接去配置类中开启记住我:
1 2 3 4 5 6 7 //授权
@ Override
protected void configure ( HttpSecurity http) throws Exception {
// ...
//记住我
http . rememberMe ();
}
然后我们进行测试,发现登录页多了一个记住我功能,我们登陆成功后关闭浏览器,再重新打开浏览器,用户依旧存在,不用再次登录。
那么这个功能是怎么实现的呢?
其实很简单,就是用户登录成功后,将cookie发送给浏览器保存,以后登录带上这个cookie,只要通过检查就可以免登录了(cookie默认保留14天)。如果点击注销,则会删除这个cookie。
首页定制
我们现在使用的登录页面时Security自带的,不是很美观,我们想要使用我们自己编写的界面,那么应该怎么做呢?
在登录页配置后面加上loginPage("/toLogin")即可。即:http.formLogin().loginPage("/toLogin");
这时候的前端页面也应当指向我们自定义的login请求:
1 2 3 < a class = "item" th:href = "@{/toLogin}" >
< i class = "address card icon" ></ i > 登录
</ a >
假设我们的前端的请求不是/toLogin,而是/login,那么应该怎么写呢?默认是/login方式,但是默认的已经被我们自定义的取消的。我们可以这么写:
1 2 3 4 5 6 7 8 //权限不足默认到登录页,并开启登录页面
//login
//定制登录页 loginPage("/toLogin") 参数表示登录页请求地址
// loginProcessingUrl("/login") 参数表示前端请求地址
//可加usernameParameter("username")
// .passwordParameter("password") 绑定前端输入框名,防止输入框name与Security默认的不一致
http . formLogin (). loginPage ( "/toLogin" ). usernameParameter ( "username" )
. passwordParameter ( "password" ). loginProcessingUrl ( "/login" );
由于使用了自定义的登录界面,“记住我”功能也消失了,我们需要自定义,在配置类中编写:
1 2 //开启记住我功能 cookies,自定义接受前端参数
http . rememberMe (). rememberMeParameter ( "remember" );
同时在登录页添加记住我的组件:
1 < input type = "checkbox" name = "remember" > 记住我
然后进行测试即可!
1.6 代码节选
前端页面设计采用了SemanticUI进行编写。
完整配置代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 @ EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
//授权
@ Override
protected void configure ( HttpSecurity http ) throws Exception {
//首页所有页可以访问,但是功能页需要访问权限
http . authorizeRequests ()
. antMatchers ( "/" ). permitAll ()
. antMatchers ( "/level1/**" ). hasAnyRole ( "vip1" )
. antMatchers ( "/level2/**" ). hasAnyRole ( "vip2" )
. antMatchers ( "/level3/**" ). hasAnyRole ( "vip3" );
//权限不足默认到登录页,并开启登录页面
//login
//定制登录页 loginPage("/toLogin")
//可加usernameParameter("username")
// .passwordParameter("password")绑定前端输入框名
http . formLogin (). loginPage ( "/toLogin" ). usernameParameter ( "username" )
. passwordParameter ( "password" ). loginProcessingUrl ( "/login" );
//防止网站攻击
http . csrf (). disable (); //关闭csrf
//开启注销,并跳转至首页
http . logout (). logoutSuccessUrl ( "/" );
//开启记住我功能 cookies,自定义接受前端参数
http . rememberMe (). rememberMeParameter ( "remember" );
}
//认证
//密码编码:PasswordEncoder
//在Spring Security 5.x中新增了多种加密方式
@ Override
protected void configure ( AuthenticationManagerBuilder auth ) throws Exception {
//真实场景从数据库读取
auth . inMemoryAuthentication (). passwordEncoder ( new BCryptPasswordEncoder ())
. withUser ( "yang" ). password ( new BCryptPasswordEncoder (). encode ( "123456" )). roles ( "vip2" , "vip3" )
. and ()
. withUser ( "root" ). password ( new BCryptPasswordEncoder (). encode ( "123456" )). roles ( "vip1" , "vip2" , "vip3" )
. and ()
. withUser ( "guest" ). password ( new BCryptPasswordEncoder (). encode ( "123456" )). roles ( "vip1" );
}
}
前端登录界面节选:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 < div class = "ui placeholder segment" >
< div class = "ui column very relaxed stackable grid" >
< div class = "column" >
< div class = "ui form" >
< form th:action = "@{/login}" method = "post" >
< div class = "field" >
< label >Username</ label >
< div class = "ui left icon input" >
< input type = "text" placeholder = "Username" name = "username" >
< i class = "user icon" ></ i >
</ div >
</ div >
< div class = "field" >
< label >Password</ label >
< div class = "ui left icon input" >
< input type = "password" name = "password" >
< i class = "lock icon" ></ i >
</ div >
</ div >
< div class = "field" >
< input type = "checkbox" name = "remember" >记住我
</ div >
< input type = "submit" class = "ui blue submit button" />
</ form >
</ div >
</ div >
</ div >
</ div >
2. Shiro
2.1 什么是Shiro
Shiro是一个Java安全(权限)框架
Shiro可以非常容易的开发出足够好的应用,不仅可以用于JavaSE环境,还可以用于JavaEE环境
Shiro可以完成认证、授权、加密、会话管理、Web集成、缓存等
官网:Shiro
2.2 有哪些功能
Authentication:身份认证、登录
Authorization:授权验证
Session Manage:会话管理,即用户登录后就是第一次会话,在没退出之前,他的所有信息都在会话中
Cryptography:加密
Web Support:Web支持
Caching:缓存
Concurrency:Shiro支持多线程应用的并发验证,即:在一个线程中开启另外一个线程,能把权限自动传播过去
Testing:提供测试支持
Run As:如果允许,可以使一个用户伪装成另一个用户登录
Remember Me:记住我
2.3 Shiro 快速开始
官方GitHub快速开始代码:ShiroQuickStart
官方快速开始文档:ShiroQuickStartDoc
根据官方文档,我们先创建一个Maven项目,然后倒入以下依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 < dependency >
< groupId >org.apache.shiro</ groupId >
< artifactId >shiro-core</ artifactId >
< version >1.4.1</ version >
</ dependency >
<!-- Shiro uses SLF4J for logging. We'll use the 'simple' binding
in this example app. See http://www.slf4j.org for more info. -->
< dependency >
< groupId >org.slf4j</ groupId >
< artifactId >slf4j-simple</ artifactId >
< version >1.7.21</ version >
</ dependency >
< dependency >
< groupId >org.slf4j</ groupId >
< artifactId >jcl-over-slf4j</ artifactId >
< version >1.7.21</ version >
</ dependency >
< dependency >
< groupId >log4j</ groupId >
< artifactId >log4j</ artifactId >
< version >1.2.17</ version >
</ dependency >
将官方的 log4j.properties 文件拷贝至我们项目的resources目录下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 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 文件拷贝至我们项目的resources目录下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 [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
将将官方的 Quickstart.java 文件拷贝至我们项目的java目录下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 import org.apache.shiro.SecurityUtils ;
import org.apache.shiro.authc.* ;
import org.apache.shiro.ini.IniSecurityManagerFactory ;
import org.apache.shiro.mgt.SecurityManager ;
import org.apache.shiro.session.Session ;
import org.apache.shiro.subject.Subject ;
import org.apache.shiro.lang.util.Factory ;
import org.slf4j.Logger ;
import org.slf4j.LoggerFactory ;
/**
* Simple Quickstart application showing how to use Shiro's API.
*
* @since 0.9 RC2
*/
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 < SecurityManager > 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 );
}
}
然后我们就可以运行并在控制台查看输出日志了。
如果我们直接拷贝官方的依赖,运行会报错,这时我们需要导入common-logging依赖(Shiro默认使用的日志依赖),然后再次运行;运行后我们又发现控制台并没有按照我们的期望输出日志,这是因为我们的依赖拷贝的依赖中含有<scope>test</scope>,我们需要将<scope>test</scope>删除就可以输出期望的日志了。
2.4 Shiro的Subject分析
我们来分析一下快速开始中的Java代码。👊
首先,使用工厂模式解析配置文件 shiro.ini ,并创建 SecurityManager 对象:
1 2 3 Factory < SecurityManager > factory = new IniSecurityManagerFactory ( "classpath:shiro.ini" ) ;
SecurityManager securityManager = factory . getInstance ();
SecurityUtils . setSecurityManager (securityManager);
获取当前的用户对象Subject:
1 Subject currentUser = SecurityUtils . getSubject ();
通过当前用户获取Session(Shiro的Session,非Web中的Session):
1 2 3 4 5 6 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 + "]" );
}
判断当前的用户是否被认证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 if ( ! currentUser . isAuthenticated () ) {
// Token: 令牌
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." );
角色检查:
1 2 3 4 5 if ( currentUser . hasRole ( "schwartz" ) ) {
log . info ( "May the Schwartz be with you!" );
} else {
log . info ( "Hello, mere mortal." );
}
权限检查,粗粒度:
1 2 3 4 5 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." );
}
权限检查(比第一次检查得更具体),细粒度:
1 2 3 4 5 6 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!" );
}
账户注销:
退出系统:
常用代码:
1 2 3 4 5 6 7 Subject currentUser = SecurityUtils . getSubject ();
Session session = currentUser . getSession ();
currentUser . isAuthenticated ();
currentUser . getPrincipal ();
currentUser . hasRole ( "schwartz" );
currentUser . isPermitted ( "lightsaber:wield" );
currentUser . logout ();
2.5 SpringBoot 集成 Shiro
2.5.1 项目准备
创建SpringBoot项目,选择导入Web和Thymeleaf的启动器:
1 2 3 4 5 6 7 8 < 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 >
编写一个简单的首页,用于测试环境是否搭建成功:
1 2 3 4 5 6 7 8 9 10 11 <! DOCTYPE html >
< html lang = "en" xmlns:th = "https://www.thymeleaf.org/" >
< head >
< meta charset = "UTF-8" >
< title >Title</ title >
</ head >
< body >
< h1 >首页</ h1 >
< p th:text = "${msg}" ></ p >
</ body >
</ html >
再编写一个简单的Controller,用于界面跳转:
1 2 3 4 5 6 7 8 9 @ Controller
public class MyController {
@ RequestMapping ({ "/" , "index" })
public String toIndex ( Model model ){
model . addAttribute ( "msg" , "hello,shiro!" );
return "index" ;
}
}
最后,启动项目,访问:http://localhost:8080/检查环境是否搭建成功!
补充:
Shiro三大对象回顾:
1 2 3 4 5 <!--
Subject 用户
SecurityManager 管理所有用户
Realm 连接数据
-->
2.5.2 创建三大对象
导入整合所需依赖:
1 2 3 4 5 6 7 <!--shiro整合spring-->
<!--shiro-spring -->
< dependency >
< groupId >org.apache.shiro</ groupId >
< artifactId >shiro-spring</ artifactId >
< version >1.5.0</ version >
</ dependency >
创建包config,在包下创建ShiroConfig.java:
1 2 3 4 5 6 @ Configuration
public class ShiroConfig {
//3. 创建 ShiroFilterFactoryBean
//2. 创建 DefaultWebSecurityManager
//1. 创建 realm 对象
}
我们需要倒着来进行创建,首先创建 realm 对象。在config包下创建自定义realm类UserRealm.java:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 //自定义UserRealm extends AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
//授权
@ Override
protected AuthorizationInfo doGetAuthorizationInfo ( PrincipalCollection principalCollection ) {
System . out . println ( "执行了=>授权doGetAuthorizationInfo" );
return null ;
}
//认证
@ Override
protected AuthenticationInfo doGetAuthenticationInfo ( AuthenticationToken token ) throws AuthenticationException {
System . out . println ( "执行了=>认证doGetAuthenticationInfo" );
return null ;
}
}
PS: Shiro会先执行认证,再执行授权!
将自定义的UserRealm类添加到Spring容器中(在ShiroConfig类中编写):
1 2 3 4 5 //创建realm对象, 自定义类 :1
@ Bean ( name = "userRealm" )
public UserRealm userRealm (){
return new UserRealm () ;
}
创建 DefaultWebSecurityManager :
1 2 3 4 5 6 7 8 9 //DefaultWebSecurityManager :2
@ Bean ( name = "securityManager" )
public DefaultWebSecurityManager getDefaultWebSecurityManager ( @ Qualifier ( "userRealm" ) UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager () ;
//关联UserRealm
// 也可以不使用@Qualifier,直接securityManager.setRealm(userRealm());
securityManager . setRealm (userRealm);
return securityManager ;
}
创建 ShiroFilterFactoryBean:
1 2 3 4 5 6 7 8 //ShiroFilterFactoryBean :3
@ Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean ( @ Qualifier ( "securityManager" ) DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean () ;
//设置安全管理器
bean . setSecurityManager (defaultWebSecurityManager);
return bean ;
}
2.5.3 页面拦截
编写两个前端页面:
add.html:
1 2 3 4 5 6 7 8 9 10 <! DOCTYPE html >
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< title >Title</ title >
</ head >
< body >
< h1 >ADD</ h1 >
</ body >
</ html >
update.html:
1 2 3 4 5 6 7 8 9 10 <! DOCTYPE html >
< html lang = "en" >
< head >
< meta charset = "UTF-8" >
< title >Title</ title >
</ head >
< body >
< h1 >UPDATE</ h1 >
</ body >
</ html >
在控制器中编写跳转代码:
1 2 3 4 5 6 7 8 9 @ RequestMapping ( "/user/add" )
public String add (){
return "user/add" ;
}
@ RequestMapping ( "/user/update" )
public String update (){
return "user/update" ;
}
在index.html中编写用于点击的超链接:
1 2 3 < a th:href = "@{/user/add}" >add</ a >
< a th:href = "@{/user/update}" >update</ a >
在配置类中添加Shiro过滤器:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 //ShiroFilterFactoryBean :3
@ Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean ( @ Qualifier ( "securityManager" ) DefaultWebSecurityManager defaultWebSecurityManager){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean () ;
//设置安全管理器
bean . setSecurityManager (defaultWebSecurityManager);
//添加shiro内置过滤器
/*
anon:无需认证就能访问
authc:必须认证才能访问
user:必须拥有“记住我”功能才能使用
perms:拥有对某个资源的权限才能访问
role:拥有某个角色权限才能访问
*/
//拦截
Map < String , String > filterMap = new LinkedHashMap <> () ;
filterMap . put ( "/user/add" , "authc" );
filterMap . put ( "/user/update" , "authc" );
bean . setFilterChainDefinitionMap (filterMap);
return bean ;
}
然后启动项目,运行测试,进入index界面后,我们点击界面上的add或update超链接,这个时候界面不会跳转至对应的界面,而是报错。这是因为我们进行的拦截设置,我们当前并没有权限。
但实际场景应该是:没有权限跳转至登录界面,而不是给用户报错。因此我们需要编写一个登录界面login.html:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 <! DOCTYPE html >
< html lang = "en" xmlns:th = "https://www.thymeleaf.org/" >
< head >
< meta charset = "UTF-8" >
< title >Title</ title >
</ head >
< body >
< h1 >登录</ h1 >
< hr >
<!-- 消息提示 -->
< p th:text = "${msg}" style = "color: red" ></ p >
< form th:action = "@{/login}" >
< p >用户名:< input type = "text" name = "username" ></ p >
< p >密码:< input type = "password" name = "password" ></ p >
< p >< input type = "submit" value = "提交" ></ p >
</ form >
</ body >
</ html >
添加跳转至登录界面的控制器:
1 2 3 4 @ RequestMapping ( "/toLogin" )
public String toLogin (){
return "login" ;
}
在ShiroConfig配置类中设置登录的请求:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 @ Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean ( @ Qualifier ( "securityManager" ) DefaultWebSecurityManager defaultWebSecurityManager){
// ...
// filterMap.put("/user/add","authc");
// filterMap.put("/user/update","authc");
// 将上述代码二合一,支持通配符
//授权,未被授权会跳转到未授权界面
filterMap . put ( "/user/*" , "authc" );
bean . setFilterChainDefinitionMap (filterMap);
//设置登录请求
bean . setLoginUrl ( "/toLogin" );
return bean ;
}
然后,我们可以进行测试,发现未登录时会跳转至登录界面。
2.5.4 用户认证
在控制器中编写登录逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 @ RequestMapping ( "/login" )
public String login ( String username , String password , Model model){
//获取当前的用户
Subject subject = SecurityUtils . getSubject ();
//封装用户的登录数据
UsernamePasswordToken token = new UsernamePasswordToken (username , password) ;
try {
subject . login (token); //执行登录的方法,如果没有异常就登录成功
return "index" ;
} catch ( UnknownAccountException e ) { //用户名不存在
model . addAttribute ( "msg" , "用户名不存在!" );
return "login" ;
} catch ( IncorrectCredentialsException e ){
model . addAttribute ( "msg" , "密码不正确!" );
return "login" ;
}
}
在 UserRealm 类中编写用户认证逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 //认证
@ Override
protected AuthenticationInfo doGetAuthenticationInfo ( AuthenticationToken token) throws AuthenticationException {
System . out . println ( "执行了=>认证doGetAuthenticationInfo" );
// 模拟登录数据
String username = "root" ;
String password = "123456" ;
UsernamePasswordToken userToken = (UsernamePasswordToken) token ;
if ( ! userToken . getUsername . equals (username) ){ //账户不存在
return null ; //UnknownAccountException
}
return new SimpleAuthenticationInfo ( "" , password , "" ) ;
}
2.5.5 整合数据库
导入依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 <!--lombok-->
< dependency >
< groupId >org.projectlombok</ groupId >
< artifactId >lombok</ artifactId >
</ dependency >
<!--MySQL驱动-->
< dependency >
< groupId >mysql</ groupId >
< artifactId >mysql-connector-java</ artifactId >
</ dependency >
<!--log4j-->
< dependency >
< groupId >log4j</ groupId >
< artifactId >log4j</ artifactId >
< version >1.2.17</ version >
</ dependency >
<!--druid-->
< dependency >
< groupId >com.alibaba</ groupId >
< artifactId >druid</ artifactId >
< version >1.1.21</ version >
</ dependency >
<!--mybatis springboot-->
< dependency >
< groupId >org.mybatis.spring.boot</ groupId >
< artifactId >mybatis-spring-boot-starter</ artifactId >
< version >2.1.1</ version >
</ dependency >
编写全局配置文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 spring :
datasource :
username : root
password : 123456
#?serverTimezone=UTC解决时区的报错
url : jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name : com.mysql.cj.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:
1 2 mybatis.type-aliases-package = com.yang.pojo
mybatis.mapper-locations = classpath:mapper/*.xml
创建POJO(Account.java):
1 2 3 4 5 6 7 8 9 10 @ Data
@ AllArgsConstructor
@ NoArgsConstructor
public class Account {
private int id ;
private String name ;
private float money ;
private String password ;
}
编写mapper接口:
1 2 3 4 5 6 @ Repository
@ Mapper
public interface AccountMapper {
public Account queryAccountByName ( String name );
}
编写MyBatis映射文件:
1 2 3 4 5 6 7 8 9 10 11 <? 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 = "com.yang.mapper.AccountMapper" >
< select id = "queryAccountByName" parameterType = "String" resultType = "Account" >
select * from account where name = #{name}
</ select >
</ mapper >
编写Service层的接口和实现类:
1 2 3 public interface AccountService {
public Account queryAccountByName ( String name );
}
1 2 3 4 5 6 7 8 9 10 11 @ Service
public class AccountServiceImpl implements AccountService {
@ Autowired
AccountMapper accountMapper ;
@ Override
public Account queryAccountByName ( String name ) {
return accountMapper . queryAccountByName (name);
}
}
改造UserRealm 类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 //自定义UserRealm extends AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
@ Autowired
AccountService accountService ;
//授权
@ Override
protected AuthorizationInfo doGetAuthorizationInfo ( PrincipalCollection principalCollection ) {
System . out . println ( "执行了=>授权doGetAuthorizationInfo" );
return null ;
}
//认证
@ Override
protected AuthenticationInfo doGetAuthenticationInfo ( AuthenticationToken token ) throws AuthenticationException {
System . out . println ( "执行了=>认证doGetAuthenticationInfo" );
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
Account account = accountService . queryAccountByName ( userToken . getUsername ());
if (account == null ){ //账户不存在
return null ; //UnknownAccountException
}
//可以加密 MD5、MD5盐值加密
//密码认证,shiro自行验证 密码加密
return new SimpleAuthenticationInfo ( "" , account . getPassword (), "" );
}
}
2.5.6 MD5盐值加密密码
在上述案例中,我们只使用的简单的认证,即使是明文密码也可以通过认证,我们还可以设置其他的加密方法。
查看有哪些加密方法:
可以去 UserRealm 的父类 AuthorizingRealm 的父类 AuthenticatingRealm 中找一个方法。这个方法可以设置证书匹配器setCredentialsMatcher()。我们进入接口CredentialsMatcher,使用快捷键Ctrl + Alt + B查看接口实现类:
我们看到有许多的实现类,这些实现类就提供了加密的方法。
❓那如果我想要更换加密方式,应该怎么做呢?
在配置类中创建一个bean:
1 2 3 4 5 6 7 8 9 @ Bean
public HashedCredentialsMatcher hashedCredentialsMatcher (){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher () ;
// 使用md5 算法进行加密
hashedCredentialsMatcher . setHashAlgorithmName ( "md5" );
// 设置散列次数: 意为加密几次
hashedCredentialsMatcher . setHashIterations ( 1 );
return hashedCredentialsMatcher ;
}
将创建的bean注册到realm中:
1 2 3 4 5 6 7 //创建realm对象, 自定义类 :1
@ Bean ( name = "userRealm" )
public UserRealm userRealm (){
UserRealm userRealm = new UserRealm () ;
userRealm . setCredentialsMatcher ( hashedCredentialsMatcher ());
return userRealm ;
}
修改自定义的realm:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 //认证
@ Override
protected AuthenticationInfo doGetAuthenticationInfo ( AuthenticationToken token) throws AuthenticationException {
// ...
// 设置盐值
ByteSource salt = ByteSource . Util . bytes ( account . getName ());
// 密码使用MD5盐值加密
SimpleHash simpleHash = new SimpleHash ( "MD5" , account . getPassword (), salt) ;
// 打印解密后的密码
System . out . println ( simpleHash . toString ());
// 返回SimpleAuthenticationInfo实例,格式为(用户,用户密码,盐,当前Realm的类名)
return new SimpleAuthenticationInfo (account , simpleHash , salt , "" ) ;
}
然后我们可以重新启动项目,进行登录。测试后,可以登录成功,控制台也打印出加密后的密码:
1 dbb1c112a931eeb16299d9de1f30161d
我们可以使用MD5在线穷举破解( MD5不可逆 ,这只是穷举法破解出来的),得到解密后的密码:
我们发现,使用了盐值后,密码变成了 盐值 + 实际密码 的格式。😎
❓那么我们确定是真的被加密了呢?我们可以在下方位置打一个断点:
然后使用Debug模式启动项目,进入首页,点击登录,输入用户名和密码。在控制台中找到this,点开它;找到credentialsMatcher,点开它。我们可以发现,我们前面进行的配置都在这里显示了。如果使用最开始的方式,不进行加密,这里会是空的。💪
PS:
MD5在线加密:MD5在线加密
MD5在线穷举破解:MD5在线解密破解
2.5.7 用户拦截与授权
经过上面的测试,我们多次在控制台上看到以下信息:
1 2 执行了=>认证doGetAuthenticationInfo
执行了=>授权doGetAuthorizationInfo
表明 Shiro会先执行认证,再执行授权!
拦截
在ShiroConfig类的ShiroFilterFactoryBean中添加以下代码:
1 2 3 4 5 6 7 8 9 10 @ Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean ( @ Qualifier ( "securityManager" ) DefaultWebSecurityManager defaultWebSecurityManager){
// ...
//授权,未被授权会跳转到未授权界面
filterMap . put ( "/user/add" , "perms[account:add]" );
filterMap . put ( "/user/update" , "perms[account:update]" );
filterMap . put ( "/user/*" , "authc" );
// ...
return bean ;
}
然后我们运行程序,并登陆,点击页面中的 add 或 update 链接,这时候页面会跳转至错误页面。状态码是401,显示未授权。
但是真实业务中,不应该跳转至错误页面,应该跳转至权限不足页面。因此,我们需要编写Controller(模拟跳转至错误页面):
1 2 3 4 5 @ RequestMapping ( "/noauth" )
@ ResponseBody
public String unauthorized (){
return "当前用户权限不足!" ;
}
同时,需要在ShiroConfig类的ShiroFilterFactoryBean中设置权限不足界面:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 @ Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean ( @ Qualifier ( "securityManager" ) DefaultWebSecurityManager defaultWebSecurityManager){
// ...
//授权,未被授权会跳转到未授权界面
filterMap . put ( "/user/add" , "perms[account:add]" );
filterMap . put ( "/user/update" , "perms[account:update]" );
filterMap . put ( "/user/*" , "authc" );
// ...
//设置权限不足界面
bean . setUnauthorizedUrl ( "/noauth" );
return bean ;
}
然后,我们可以再进行测试。
运行程序,并登陆,点击页面中的 add 或 update 链接,这时候页面会跳转至我们设定的未授权界面。
授权
我们已经让某些界面需要权限才能够访问,那么应该怎么给用户赋予一些权限呢?
在UserRealm类中授权方法中添加权限赋予代码:
1 2 3 4 5 6 7 8 9 10 11 12 //授权
@ Override
protected AuthorizationInfo doGetAuthorizationInfo ( PrincipalCollection principalCollection) {
System . out . println ( "执行了=>授权doGetAuthorizationInfo" );
//给资源进行授权
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo () ;
//授予所有用户add权限
info . addStringPermission ( "account:add" );
return info ;
}
运行项目,选择任一用户登录系统。这时候所有的用户都被赋予 add 权限,相当于系统的默认权限。同时,在实际的业务操作中,用户的权限信息是储存在数据库中的,并且不同用于拥有不同的权限。为了达到这个目的,我们需要在现有数据库的用户表中添加perms字段,表示用户所拥有的对应权限。
然后,我们需要给我们的实体类增加一个属性,用来表示用户的权限:
1 2 3 4 5 6 7 8 9 10 11 @ Data
@ AllArgsConstructor
@ NoArgsConstructor
public class Account {
private int id ;
private String name ;
private float money ;
private String password ;
private String perms ;
}
更改UserRealm类中认证方法的返回值,使授权方法可以获取到当前用户:
1 return new SimpleAuthenticationInfo (account , account . getPassword (), "" ) ;
更改UserRealm类中授权方法的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //授权
@ Override
protected AuthorizationInfo doGetAuthorizationInfo ( PrincipalCollection principalCollection) {
System . out . println ( "执行了=>授权doGetAuthorizationInfo" );
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo () ;
//授予所有用户add权限
info . addStringPermission ( "account:add" );
//获取当前登录的对象
Subject subject = SecurityUtils . getSubject ();
/*获取认证返回的用户信息
new SimpleAuthenticationInfo(account,simpleHash,salt,"")
即:获取account
*/
Account currentAccount = (Account) subject . getPrincipal (); //获取Account对象
//设置当前用户的权限
info . addStringPermission ( currentAccount . getPerms ());
return info ;
}
最后,进行测试即可。
2.5.8 整合Thymeleaf
经过上面的编写与测试,我们发现用户如果没有 update 权限的情况下,进入首页也会显示 update 的链接。我们希望的应该是没有对应的权限,就不要显示对应的链接。这时候,就需要进行 Shiro 和 Thymeleaf 的整合!
首先,我们需要导入Shiro与Thymeleaf整合的依赖:
1 2 3 4 5 6 <!--shiro thymeleaf-->
< dependency >
< groupId >com.github.theborakompanioni</ groupId >
< artifactId >thymeleaf-extras-shiro</ artifactId >
< version >2.0.0</ version >
</ dependency >
然后在配置类ShiroConfig中编写getShiroDialect()并向容器中添加一个bean:
1 2 3 4 5 //整合ShiroDialect:用于整合shiro thymeleaf
@ Bean
public ShiroDialect getShiroDialect (){
return new ShiroDialect () ;
}
修改index.html前端代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 <! DOCTYPE html >
< html lang = "en" xmlns:th = "https://www.thymeleaf.org/"
xmlns:shiro = "https://www.thymeleaf.org/thymeleaf-extras-shiro" >
< head >
< meta charset = "UTF-8" >
< title >Title</ title >
</ head >
< body >
< h1 >首页</ h1 >
<!--判断用户是否登录-->
< div shiro:notAuthenticated >
< a th:href = "@{/toLogin}" >登录</ a >
</ div >
< hr >
< p th:text = "${msg}" ></ p >
< div shiro:hasPermission = "account:add" >
< a th:href = "@{/user/add}" >add</ a >
</ div >
< div shiro:hasPermission = "account:update" >
< a th:href = "@{/user/update}" >update</ a >
</ div >
</ body >
</ html >
最后,启动项目,测试即可。
在进行判断用户是否登录时,还可以从Session中进行取值判断。
更改前端代码:
1 2 3 4 <!--从session中判断值-->
< div th:if = "${session.loginAccount==null}" >
< a th:href = "@{/toLogin}" >登录</ a >
</ div >
在UserRealm类的认证方法中设置Session(这里的Session是Shiro的,不是Servlet的):
1 2 3 4 5 6 7 8 9 10 11 12 //认证
@ Override
protected AuthenticationInfo doGetAuthenticationInfo ( AuthenticationToken token) throws AuthenticationException {
System . out . println ( "执行了=>认证doGetAuthenticationInfo" );
// ...
Subject currentSubject = SecurityUtils . getSubject ();
Session session = currentSubject . getSession ();
session . setAttribute ( "loginAccount" ,account);
// ...
}
同样可以启动项目并测试,会得到相同的结果。
2.6 重要代码
自定义realm,使用了MD5盐值加密。整合Thymeleaf时,采用从Session中获取值判断有无用户登录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 //自定义UserRealm extends AuthorizingRealm
public class UserRealm extends AuthorizingRealm {
@ Autowired
AccountService accountService ;
//授权
@ Override
protected AuthorizationInfo doGetAuthorizationInfo ( PrincipalCollection principalCollection ) {
System . out . println ( "执行了=>授权doGetAuthorizationInfo" );
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo ();
//授予所有用户add权限
info . addStringPermission ( "account:add" );
//获取当前登录的对象
Subject subject = SecurityUtils . getSubject ();
/*获取认证返回的用户信息
new SimpleAuthenticationInfo(account,simpleHash,salt,"")
即:获取account
*/
Account currentAccount = (Account) subject . getPrincipal (); //获取Account对象
//设置当前用户的权限
info . addStringPermission ( currentAccount . getPerms ());
return info;
}
//认证
@ Override
protected AuthenticationInfo doGetAuthenticationInfo ( AuthenticationToken token ) throws AuthenticationException {
System . out . println ( "执行了=>认证doGetAuthenticationInfo" );
UsernamePasswordToken userToken = (UsernamePasswordToken) token;
Account account = accountService . queryAccountByName ( userToken . getUsername ());
if (account == null ){ //账户不存在
return null ; //UnknownAccountException
}
Subject currentSubject = SecurityUtils . getSubject ();
Session session = currentSubject . getSession ();
session . setAttribute ( "loginAccount" ,account);
// 设置盐值
ByteSource salt = ByteSource . Util . bytes ( account . getName ());
// 密码使用MD5盐值加密
SimpleHash simpleHash = new SimpleHash ( "MD5" , account . getPassword (), salt);
// 打印解密后的密码
System . out . println ( simpleHash . toString ());
return new SimpleAuthenticationInfo (account,simpleHash,salt, "" );
}
}
Shiro配置类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 @ Configuration
public class ShiroConfig {
//ShiroFilterFactoryBean :3
@ Bean
public ShiroFilterFactoryBean getShiroFilterFactoryBean (@ Qualifier ( "securityManager" ) DefaultWebSecurityManager defaultWebSecurityManager ){
ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean ();
//设置安全管理器
bean . setSecurityManager (defaultWebSecurityManager);
//添加shiro内置过滤器
/*
anon:无需认证就能访问
authc:必须认证才能访问
user:必须拥有“记住我”功能才能使用
perms:拥有对某个资源的权限才能访问
role:拥有某个角色权限才能访问
*/
//拦截
Map < String , String > filterMap = new LinkedHashMap <>();
// filterMap.put("/user/add","authc");
// filterMap.put("/user/update","authc");
//授权,未被授权会跳转到权限不足界面
filterMap . put ( "/user/add" , "perms[account:add]" );
filterMap . put ( "/user/update" , "perms[account:update]" );
filterMap . put ( "/user/*" , "authc" );
bean . setFilterChainDefinitionMap (filterMap);
//设置登录请求
bean . setLoginUrl ( "/toLogin" );
//设置权限不足界面
bean . setUnauthorizedUrl ( "/noauth" );
return bean;
}
//DefaultWebSecurityManager :2
@ Bean ( name = "securityManager" )
public DefaultWebSecurityManager getDefaultWebSecurityManager (@ Qualifier ( "userRealm" ) UserRealm userRealm ){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager ();
//关联UserRealm
securityManager . setRealm (userRealm);
return securityManager;
}
//创建realm对象, 自定义类 :1
@ Bean ( name = "userRealm" )
public UserRealm userRealm (){
UserRealm userRealm = new UserRealm ();
userRealm . setCredentialsMatcher ( hashedCredentialsMatcher ());
return userRealm;
}
@ Bean
public HashedCredentialsMatcher hashedCredentialsMatcher (){
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher ();
// 使用md5 算法进行加密
hashedCredentialsMatcher . setHashAlgorithmName ( "md5" );
// 设置散列次数: 意为加密几次
hashedCredentialsMatcher . setHashIterations ( 1 );
return hashedCredentialsMatcher;
}
//整合ShiroDialect:用于整合shiro thymeleaf
@ Bean
public ShiroDialect getShiroDialect (){
return new ShiroDialect ();
}
}