一、Shiro基础知识
在学习Shiro这个框架之前,首先我们要先了解Shiro需要的基础知识:权限管理
1.1什么是权限管理?
只要有用户参与的系统一般都要有权限管理,权限管理实现对用户访问系统的控制,按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。
对权限的管理又分为两大类别:
- 用户认证
- 用户授权
1.1.1用户认证
用户认证,用户去访问系统,系统要验证用户身份的合法性
最常用的用户身份验证的方法:1、用户名密码方式、2、指纹打卡机、3、基于证书验证方法。。系统验证用户身份合法,用户方可访问系统的资源。
举个例子:
- 当我们输入了自己的淘宝的账户和密码,才能打开购物车
用户认证的流程:
- 判断该资源能否不认证就能访问【登陆页面、首页】
- 如果该资源需要认证后才能访问,那么判断该访问者是否认证了
- 如果还没有认证,那么需要返回到【登陆页面】进行认证
- 认证通过后才能访问资源
从用户认证我们可以抽取出这么几个概念
- subject主体:理解为用户,可能是程序,都要去访问系统的资源,系统需要对subject进行身份认证
- principal身份信息:通常是唯一的,一个主体还有多个身份信息,但是都有一个主身份信息(primary principal)【我们可以选择身份证认证、学生证认证等等都是我们的身份信息】
- credential凭证信息:可以是密码 、证书、指纹。
总结:主体在进行身份认证时需要提供身份信息和凭证信息。
1.1.2用户授权
用户授权,简单理解为访问控制,在用户认证通过后,系统对用户访问资源进行控制,用户具有资源的访问权限方可访问。
用户授权的流程
- 到达了用户授权环节,当然是需要用户认证之后了
- 用户访问资源,系统判断该用户是否有权限去操作该资源
- 如果该用户有权限才能够访问,如果没有权限就不能访问了
授权的过程可以简单理解为:主体认证之后,系统进行访问控制
subject必须具备资源的访问权限才可访问该资源..
权限/许可(permission) :针对资源的权限或许可,subject具有permission访问资源,如何访问/操作需要定义permission,权限比如:用户添加、用户修改、商品删除
资源可以分为两种
- 资源类型:系统的用户信息就是资源类型,相当于java类。
- 资源实例:系统中id为001的用户就是资源实例,相当于new的java对象。
1.2权限管理模型
一般地,我们可以抽取出这么几个模型:
- 主体(账号、密码)
- 资源(资源名称、访问地址)
- 权限(权限名称、资源id)
- 角色(角色名称)
- 角色和权限关系(角色id、权限id)
- 主体和角色关系(主体id、角色id)
通常企业开发中将资源和权限表合并为一张权限表,如下:
- 资源(资源名称、访问地址)
- 权限(权限名称、资源id)
合并为:
- 权限(权限名称、资源名称、资源访问地址)
1.3分配权限
用户需要分配相应的权限才可访问相应的资源。权限是对于资源的操作许可。
通常给用户分配资源权限需要将权限信息持久化,比如存储在关系数据库中。把用户信息、权限管理、用户分配的权限信息写到数据库(权限数据模型)
1.3.1基于角色访问控制
RBAC(role based access control),基于角色的访问控制。
//如果该user是部门经理则可以访问if中的代码
if(user.hasRole('部门经理')){
//系统资源内容
//用户报表查看
}
角色针对人划分的,人作为用户在系统中属于活动内容,如果该 角色可以访问的资源出现变更,需要修改你的代码了,
if(user.hasRole('部门经理') || user.hasRole('总经理') ){
//系统资源内容
//用户报表查看
}
基于角色的访问控制是不利于系统维护(可扩展性不强)。
1.3.2基于资源的访问控制
RBAC(Resource based access control),基于资源的访问控制。
资源在系统中是不变的,比如资源有:类中的方法,页面中的按钮。
对资源的访问需要具有permission权限,代码可以写为:
if(user.hasPermission ('用户报表查看(权限标识符)')){
//系统资源内容
//用户报表查看
}
建议使用基于资源的访问控制实现权限管理。
二、 粗粒度和细粒度权限
细粒度权限管理:对资源实例的权限管理。资源实例就资源类型的具体化,比如:用户id为001的修改连接,1110班的用户信息、行政部的员工。细粒度权限管理就是数据级别的权限管理。
粗粒度权限管理比如:超级管理员可以访问户添加页面、用户信息等全部页面。部门管理员可以访问用户信息页面包括 页面中所有按钮。
粗粒度和细粒度例子:
系统有一个用户列表查询页面,对用户列表查询分权限,
如果粗颗粒管理,张三和李四都有用户列表查询的权限,张三和李四都可以访问用户列表查询。
进一步进行细颗粒管理,张三(行政部)和李四(开发部)只可以查询自己本部门的用户信息。
张三只能查看行政部 的用户信息,李四只能查看开发部门的用户信息。
细粒度权限管理就是数据级别的权限管理。
2.1如何实现粗粒度权限管理?
粗粒度权限管理比较容易将权限管理的代码抽取出来在系统架构级别统一处理。比如:通过springmvc的拦截器实现授权。
对细粒度权限管理在数据级别是没有共性可言,针对细粒度权限管理就是系统业务逻辑的一部分,在业务层去处理相对比较简单
比如:部门经理只查询本部门员工信息,在service接口提供一个部门id的参数,controller中根据当前用户的信息得到该 用户属于哪个部门,调用service时将部门id传入service,实现该用户只查询本部门的员工。
2.1.1基于URL拦截
基于url拦截的方式实现在实际开发中比较常用的一种方式。
对于web系统,通过filter过虑器实现url拦截,也可以springmvc的拦截器实现基于url的拦截。
2.2.2使用权限管理框架实现
对于粗粒度权限管理,建议使用优秀权限管理框架来实现,节省开发成功,提高开发效率。
shiro就是一个优秀权限管理框架。
三、回顾URL拦截
我们在学习的路途上也是使用过几次URL对权限进行拦截的
当时我们做了权限的增删该查的管理系统,但是在权限表中是没有把资源添加进去,我们使用的是Map集合来进行替代的。
http://blog.csdn.net/hon_3y/article/details/61926175
随后,我们学习了动态代理和注解,我们也做了一个基于注解的拦截
- 在Controller得到service对象的时候,service工厂返回的是一个动态代理对象回去
- Controller拿着代理对象去调用方法,代理对象就会去解析该方法上是否有注解
- 如果有注解,那么就需要我们进行判断该主体是否认证了,如果认证了就判断该主体是否有权限
- 当我们解析出该主体的权限和我们注解的权限是一致的时候,才放行!
http://blog.csdn.net/hon_3y/article/details/70767050
流程:
3.1认证的JavaBean
我们之前认证都是放在默认的Javabean对象上的,现在既然我们准备学Shiro了,我们就得专业一点,弄一个专门存储认证信息的JavaBean
/**
* 用户身份信息,存入session 由于tomcat将session会序列化在本地硬盘上,所以使用Serializable接口
*
* @author Thinkpad
*
*/
public class ActiveUser implements java.io.Serializable {
private String userid;//用户id(主键)
private String usercode;// 用户账号
private String username;// 用户名称
private List<SysPermission> menus;// 菜单
private List<SysPermission> permissions;// 权限
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getUsercode() {
return usercode;
}
public void setUsercode(String usercode) {
this.usercode = usercode;
}
public String getUserid() {
return userid;
}
public void setUserid(String userid) {
this.userid = userid;
}
public List<SysPermission> getMenus() {
return menus;
}
public void setMenus(List<SysPermission> menus) {
this.menus = menus;
}
public List<SysPermission> getPermissions() {
return permissions;
}
public void setPermissions(List<SysPermission> permissions) {
this.permissions = permissions;
}
}
认证的服务
@Override
public ActiveUser authenticat(String userCode, String password)
throws Exception {
/**
认证过程:
根据用户身份(账号)查询数据库,如果查询不到用户不存在
对输入的密码 和数据库密码 进行比对,如果一致,认证通过
*/
//根据用户账号查询数据库
SysUser sysUser = this.findSysUserByUserCode(userCode);
if(sysUser == null){
//抛出异常
throw new CustomException("用户账号不存在");
}
//数据库密码 (md5密码 )
String password_db = sysUser.getPassword();
//对输入的密码 和数据库密码 进行比对,如果一致,认证通过
//对页面输入的密码 进行md5加密
String password_input_md5 = new MD5().getMD5ofStr(password);
if(!password_input_md5.equalsIgnoreCase(password_db)){
//抛出异常
throw new CustomException("用户名或密码 错误");
}
//得到用户id
String userid = sysUser.getId();
//根据用户id查询菜单
List<SysPermission> menus =this.findMenuListByUserId(userid);
//根据用户id查询权限url
List<SysPermission> permissions = this.findPermissionListByUserId(userid);
//认证通过,返回用户身份信息
ActiveUser activeUser = new ActiveUser();
activeUser.setUserid(sysUser.getId());
activeUser.setUsercode(userCode);
activeUser.setUsername(sysUser.getUsername());//用户名称
//放入权限范围的菜单和url
activeUser.setMenus(menus);
activeUser.setPermissions(permissions);
return activeUser;
}
Controller处理认证,如果身份认证成功,那么把认证信息存储在Session中
@RequestMapping("/login")
public String login(HttpSession session, String randomcode,String usercode,String password)throws Exception{
//校验验证码,防止恶性攻击
//从session获取正确验证码
String validateCode = (String) session.getAttribute("validateCode");
//输入的验证和session中的验证进行对比
if(!randomcode.equals(validateCode)){
//抛出异常
throw new CustomException("验证码输入错误");
}
//调用service校验用户账号和密码的正确性
ActiveUser activeUser = sysService.authenticat(usercode, password);
//如果service校验通过,将用户身份记录到session
session.setAttribute("activeUser", activeUser);
//重定向到商品查询页面
return "redirect:/first.action";
}
身份认证拦截器
//在执行handler之前来执行的
//用于用户认证校验、用户权限校验
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
//得到请求的url
String url = request.getRequestURI();
//判断是否是公开 地址
//实际开发中需要公开 地址配置在配置文件中
//从配置中取逆名访问url
List<String> open_urls = ResourcesUtil.gekeyList("anonymousURL");
//遍历公开 地址,如果是公开 地址则放行
for(String open_url:open_urls){
if(url.indexOf(open_url)>=0){
//如果是公开 地址则放行
return true;
}
}
//判断用户身份在session中是否存在
HttpSession session = request.getSession();
ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
//如果用户身份在session中存在放行
if(activeUser!=null){
return true;
}
//执行到这里拦截,跳转到登陆页面,用户进行身份认证
request.getRequestDispatcher("/WEB-INF/jsp/login.jsp").forward(request, response);
//如果返回false表示拦截不继续执行handler,如果返回true表示放行
return false;
}
授权拦截器
//在执行handler之前来执行的
//用于用户认证校验、用户权限校验
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
//得到请求的url
String url = request.getRequestURI();
//判断是否是公开 地址
//实际开发中需要公开 地址配置在配置文件中
//从配置中取逆名访问url
List<String> open_urls = ResourcesUtil.gekeyList("anonymousURL");
//遍历公开 地址,如果是公开 地址则放行
for(String open_url:open_urls){
if(url.indexOf(open_url)>=0){
//如果是公开 地址则放行
return true;
}
}
//从配置文件中获取公共访问地址
List<String> common_urls = ResourcesUtil.gekeyList("commonURL");
//遍历公用 地址,如果是公用 地址则放行
for(String common_url:common_urls){
if(url.indexOf(common_url)>=0){
//如果是公开 地址则放行
return true;
}
}
//获取session
HttpSession session = request.getSession();
ActiveUser activeUser = (ActiveUser) session.getAttribute("activeUser");
//从session中取权限范围的url
List<SysPermission> permissions = activeUser.getPermissions();
for(SysPermission sysPermission:permissions){
//权限的url
String permission_url = sysPermission.getUrl();
if(url.indexOf(permission_url)>=0){
//如果是权限的url 地址则放行
return true;
}
}
//执行到这里拦截,跳转到无权访问的提示页面
request.getRequestDispatcher("/WEB-INF/jsp/refuse.jsp").forward(request, response);
//如果返回false表示拦截不继续执行handler,如果返回true表示放行
return false;
}
拦截器配置:
<!--拦截器 -->
<mvc:interceptors>
<mvc:interceptor>
<!-- 用户认证拦截 -->
<mvc:mapping path="/**" />
<bean class="cn.itcast.ssm.controller.interceptor.LoginInterceptor"></bean>
</mvc:interceptor>
<mvc:interceptor>
<!-- 授权拦截 -->
<mvc:mapping path="/**" />
<bean class="cn.itcast.ssm.controller.interceptor.PermissionInterceptor"></bean>
</mvc:interceptor>
</mvc:interceptors>
四、什么是Shiro
shiro是apache的一个开源框架,是一个权限管理的框架,实现 用户认证、用户授权。
spring中有spring security (原名Acegi),是一个权限框架,它和spring依赖过于紧密,没有shiro使用简单。
shiro不依赖于spring,shiro不仅可以实现 web应用的权限管理,还可以实现c/s系统,分布式系统权限管理,shiro属于轻量框架,越来越多企业项目开始使用shiro。
Shiro架构:
- subject:主体,可以是用户也可以是程序,主体要访问系统,系统需要对主体进行认证、授权。
- securityManager:安全管理器,主体进行认证和授权都 是通过securityManager进行。
- authenticator:认证器,主体进行认证最终通过authenticator进行的。
- authorizer:授权器,主体进行授权最终通过authorizer进行的。
- sessionManager:web应用中一般是用web容器对session进行管理,shiro也提供一套session管理的方式。
- SessionDao: 通过SessionDao管理session数据,针对个性化的session数据存储需要使用sessionDao。
- cache Manager:缓存管理器,主要对session和授权数据进行缓存,比如将授权数据通过cacheManager进行缓存管理,和ehcache整合对缓存数据进行管理。
- realm:域,领域,相当于数据源,通过realm存取认证、授权相关数据。
cryptography:密码管理,提供了一套加密/解密的组件,方便开发。比如提供常用的散列、加/解密等功能。
- 比如md5散列算法。
五、为什么使用Shiro
我们在使用URL拦截的时候,要将所有的URL都配置起来,繁琐、不易维护
而我们的Shiro实现系统的权限管理,有效提高开发效率,从而降低开发成本。
六、Shiro认证
6.1导入jar包
我们使用的是Maven的坐标就行了
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-web</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-ehcache</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-quartz</artifactId>
<version>1.2.3</version>
</dependency>
当然了,我们也可以把Shiro相关的jar包全部导入进去
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-all</artifactId>
<version>1.2.3</version>
</dependency>
6.2Shiro认证流程
6.2.1通过配置文件创建工厂
// 用户登陆和退出
@Test
public void testLoginAndLogout() {
// 创建securityManager工厂,通过ini配置文件创建securityManager工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory(
"classpath:shiro-first.ini");
// 创建SecurityManager
SecurityManager securityManager = factory.getInstance();
// 将securityManager设置当前的运行环境中
SecurityUtils.setSecurityManager(securityManager);
// 从SecurityUtils里边创建一个subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备token(令牌)
// 这里的账号和密码 将来是由用户输入进去
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
"111111");
try {
// 执行认证提交
subject.login(token);
} catch (AuthenticationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 是否认证通过
boolean isAuthenticated = subject.isAuthenticated();
System.out.println("是否认证通过:" + isAuthenticated);
// 退出操作
subject.logout();
// 是否认证通过
isAuthenticated = subject.isAuthenticated();
System.out.println("是否认证通过:" + isAuthenticated);
}
6.3小结
ModularRealmAuthenticator作用进行认证,需要调用realm查询用户信息(在数据库中存在用户信息)
ModularRealmAuthenticator进行密码对比(认证过程)。
realm:需要根据token中的身份信息去查询数据库(入门程序使用ini配置文件),如果查到用户返回认证信息,如果查询不到返回null。
6.4自定义realm
从第一个认证程序我们可以看见,我们所说的流程,是认证器去找realm去查询我们相对应的数据。而默认的realm是直接去与配置文件来比对的,一般地,我们在开发中都是让realm去数据库中比对。
因此,我们需要自定义realm
public class CustomRealm extends AuthorizingRealm {
// 设置realm的名称
@Override
public void setName(String name) {
super.setName("customRealm");
}
// 用于认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
// token是用户输入的
// 第一步从token中取出身份信息
String userCode = (String) token.getPrincipal();
// 第二步:根据用户输入的userCode从数据库查询
// ....
// 如果查询不到返回null
//数据库中用户账号是zhangsansan
/*if(!userCode.equals("zhangsansan")){//
return null;
}*/
// 模拟从数据库查询到密码
String password = "111112";
// 如果查询到返回认证信息AuthenticationInfo
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
userCode, password, this.getName());
return simpleAuthenticationInfo;
}
// 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
}
6.5配置realm
需要在shiro-realm.ini配置realm注入到securityManager中。
6.6测试自定义realm
同上边的入门程序,需要更改ini配置文件路径:
同上边的入门程序,需要更改ini配置文件路径:
Factory<SecurityManager> factory = new IniSecurityManagerFactory(
"classpath:shiro-realm.ini");
6.7散列算法
我们如果知道md5,我们就会知道md5是不可逆的,但是如果设置了一些安全性比较低的密码:111111...即时是不可逆的,但还是可以通过暴力算法来得到md5对应的明文...
建议对md5进行散列时加salt(盐),进行加密相当 于对原始密码+盐进行散列。\
正常使用时散列方法:
- 在程序中对原始密码+盐进行散列,将散列值存储到数据库中,并且还要将盐也要存储在数据库中。
测试:
public class MD5Test {
public static void main(String[] args) {
//原始 密码
String source = "111111";
//盐
String salt = "qwerty";
//散列次数
int hashIterations = 2;
//上边散列1次:f3694f162729b7d0254c6e40260bf15c
//上边散列2次:36f2dfa24d0a9fa97276abbe13e596fc
//构造方法中:
//第一个参数:明文,原始密码
//第二个参数:盐,通过使用随机数
//第三个参数:散列的次数,比如散列两次,相当 于md5(md5(''))
Md5Hash md5Hash = new Md5Hash(source, salt, hashIterations);
String password_md5 = md5Hash.toString();
System.out.println(password_md5);
//第一个参数:散列算法
SimpleHash simpleHash = new SimpleHash("md5", source, salt, hashIterations);
System.out.println(simpleHash.toString());
}
}
6.8自定义realm支持md5
自定义realm
public class CustomRealmMd5 extends AuthorizingRealm {
// 设置realm的名称
@Override
public void setName(String name) {
super.setName("customRealmMd5");
}
// 用于认证
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
// token是用户输入的
// 第一步从token中取出身份信息
String userCode = (String) token.getPrincipal();
// 第二步:根据用户输入的userCode从数据库查询
// ....
// 如果查询不到返回null
// 数据库中用户账号是zhangsansan
/*
* if(!userCode.equals("zhangsansan")){// return null; }
*/
// 模拟从数据库查询到密码,散列值
String password = "f3694f162729b7d0254c6e40260bf15c";
// 从数据库获取salt
String salt = "qwerty";
//上边散列值和盐对应的明文:111111
// 如果查询到返回认证信息AuthenticationInfo
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
userCode, password, ByteSource.Util.bytes(salt), this.getName());
return simpleAuthenticationInfo;
}
// 用于授权
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
// TODO Auto-generated method stub
return null;
}
}
配置文件:
测试:
// 自定义realm实现散列值匹配
@Test
public void testCustomRealmMd5() {
// 创建securityManager工厂,通过ini配置文件创建securityManager工厂
Factory<SecurityManager> factory = new IniSecurityManagerFactory(
"classpath:shiro-realm-md5.ini");
// 创建SecurityManager
SecurityManager securityManager = factory.getInstance();
// 将securityManager设置当前的运行环境中
SecurityUtils.setSecurityManager(securityManager);
// 从SecurityUtils里边创建一个subject
Subject subject = SecurityUtils.getSubject();
// 在认证提交前准备token(令牌)
// 这里的账号和密码 将来是由用户输入进去
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan",
"222222");
try {
// 执行认证提交
subject.login(token);
} catch (AuthenticationException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
// 是否认证通过
boolean isAuthenticated = subject.isAuthenticated();
System.out.println("是否认证通过:" + isAuthenticated);
}
七、总结
- 用户认证和用户授权是Shiro的基础,用户认证其实上就是登陆操作、用户授权实际上就是对资源拦截的操作。
- 权限管理的模型一般我们都将资源放在权限表中进行管理起来。
- 我们可以基于角色拦截,也可以基于资源拦截。要是基于角色拦截的话,那么如果角色的权限发生变化了,那就需要修改代码了。推荐使用基于资源进行拦截
- 这次URL拦截,我们使用一个JavaBean来封装所有的认证信息。当用户登陆了之后,我们就把用户对菜单栏的访问、对资源的访问权限都封装到该JavaBean中
- 当使用拦截器进行用户认证的时候,我们只要判断Session域有没有JavaBen对象即可了。
- 当时候拦截器进行用户授权的时候,我们要判断JavaBean中的权限是否能够访问该资源。
- 以前URL拦截的方式需要把所有的URL都在数据库进行管理。非常麻烦,不易维护。
- 我们希望Shiro去认证的时候是通过realm去数据库查询数据的。而我们reaml默认是查询配置文件的数据的。
- 因此,我们需要自定义reaml,使得它是去数据库查询数据。只要继承AuthorizingRealm类就行了。
- 当然了,自定义后的reaml也需要在配置文件中写上我们的自定义reaml的位置的。
- 散列算法就是为了让密码不被别人给破解。我们可对原始的密码加盐再进行散列,这就加大了破解的难度了。
- 自定义的reaml也是支持散列算法的,相同的,还是需要我们在配置文件中配置一下就好了。
上图展示了 Shiro 认证的一个重要的过程,为了加深我们的印象,我们来自己动手来写一个例子,来验证一下,首先我们新建一个Maven工程,然后在pom.xml中引入相关依赖:
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
新建一个【AuthenticationTest】测试类:
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;
public class AuthenticationTest {
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
@Before // 在方法开始前添加一个用户
public void addUser() {
simpleAccountRealm.addAccount("wmyskxz", "123456");
}
@Test
public void testAuthentication() {
// 1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);
// 2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
Subject subject = SecurityUtils.getSubject(); // 获取当前主体
UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
subject.login(token); // 登录
// subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
subject.logout(); // 登出
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出false
}
}
运行之后可以看到预想中的效果,先输出isAuthenticated:true
表示登录认证成功,然后再输出isAuthenticated:false
表示认证失败退出登录
流程如下:
- 首先调用 Subject.login(token) 进行登录,其会自动委托给 Security Manager,调用之前必须通过 SecurityUtils.setSecurityManager() 设置;
- SecurityManager 负责真正的身份验证逻辑;它会委托给 Authenticator 进行身份验证;
- Authenticator 才是真正的身份验证者,Shiro API 中核心的身份认证入口点,此处可以自定义插入自己的实现;
- Authenticator 可能会委托给相应的 AuthenticationStrategy 进行多 Realm 身份验证,默认 ModularRealmAuthenticator 会调用 AuthenticationStrategy 进行多 Realm 身份验证;
- Authenticator 会把相应的 token 传入 Realm,从 Realm 获取身份验证信息,如果没有返回 / 抛出异常表示身份验证失败了。此处可以配置多个 Realm,将按照相应的顺序及策略进行访问。
Shiro 授权过程
跟认证过程大致相似,下面我们仍然通过代码来熟悉一下过程(引入包类似这里节约篇幅就不贴出来了):
public class AuthenticationTest {
SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
@Before // 在方法开始前添加一个用户,让它具备admin和user两个角色
public void addUser() {
simpleAccountRealm.addAccount("wmyskxz", "123456", "admin", "user");
}
@Test
public void testAuthentication() {
// 1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(simpleAccountRealm);
// 2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
Subject subject = SecurityUtils.getSubject(); // 获取当前主体
UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
subject.login(token); // 登录
// subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
// 判断subject是否具有admin和user两个角色权限,如没有则会报错
subject.checkRoles("admin","user");
// subject.checkRole("xxx"); // 报错
}
}
运行测试,能够正确看到效果。
自定义 Realm
从上面我们了解到实际进行权限信息验证的是我们的 Realm,Shiro 框架内部默认提供了两种实现,一种是查询.ini
文件的IniRealm
,另一种是查询数据库的JdbcRealm
,这两种来说都相对简单,感兴趣的可以去【这里】瞄两眼,我们着重就来介绍介绍自定义实现的 Realm 吧。
有了上面的对认证和授权的理解,我们先在合适的包下创建一个【MyRealm】类,继承 Shirot 框架的 AuthorizingRealm 类,并实现默认的两个方法:
package com.wmyskxz.demo.realm;
import org.apache.shiro.authc.*;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import java.util.*;
public class MyRealm extends AuthorizingRealm {
/**
* 模拟数据库数据
*/
Map<String, String> userMap = new HashMap<>(16);
{
userMap.put("wmyskxz", "123456");
super.setName("myRealm"); // 设置自定义Realm的名称,取什么无所谓..
}
/**
* 授权
*
* @param principalCollection
* @return
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
String userName = (String) principalCollection.getPrimaryPrincipal();
// 从数据库获取角色和权限数据
Set<String> roles = getRolesByUserName(userName);
Set<String> permissions = getPermissionsByUserName(userName);
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setStringPermissions(permissions);
simpleAuthorizationInfo.setRoles(roles);
return simpleAuthorizationInfo;
}
/**
* 模拟从数据库中获取权限数据
*
* @param userName
* @return
*/
private Set<String> getPermissionsByUserName(String userName) {
Set<String> permissions = new HashSet<>();
permissions.add("user:delete");
permissions.add("user:add");
return permissions;
}
/**
* 模拟从数据库中获取角色数据
*
* @param userName
* @return
*/
private Set<String> getRolesByUserName(String userName) {
Set<String> roles = new HashSet<>();
roles.add("admin");
roles.add("user");
return roles;
}
/**
* 认证
*
* @param authenticationToken 主体传过来的认证信息
* @return
* @throws AuthenticationException
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 1.从主体传过来的认证信息中,获得用户名
String userName = (String) authenticationToken.getPrincipal();
// 2.通过用户名到数据库中获取凭证
String password = getPasswordByUserName(userName);
if (password == null) {
return null;
}
SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo("wmyskxz", password, "myRealm");
return authenticationInfo;
}
/**
* 模拟从数据库取凭证的过程
*
* @param userName
* @return
*/
private String getPasswordByUserName(String userName) {
return userMap.get(userName);
}
}
然后我们编写测试类,来验证是否正确:
import com.wmyskxz.demo.realm.MyRealm;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.subject.Subject;
import org.junit.Test;
public class AuthenticationTest {
@Test
public void testAuthentication() {
MyRealm myRealm = new MyRealm(); // 实现自己的 Realm 实例
// 1.构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
defaultSecurityManager.setRealm(myRealm);
// 2.主体提交认证请求
SecurityUtils.setSecurityManager(defaultSecurityManager); // 设置SecurityManager环境
Subject subject = SecurityUtils.getSubject(); // 获取当前主体
UsernamePasswordToken token = new UsernamePasswordToken("wmyskxz", "123456");
subject.login(token); // 登录
// subject.isAuthenticated()方法返回一个boolean值,用于判断用户是否认证成功
System.out.println("isAuthenticated:" + subject.isAuthenticated()); // 输出true
// 判断subject是否具有admin和user两个角色权限,如没有则会报错
subject.checkRoles("admin", "user");
// subject.checkRole("xxx"); // 报错
// 判断subject是否具有user:add权限
subject.checkPermission("user:add");
}
}
运行测试,完美。
Shiro 加密
在之前的学习中,我们在数据库中保存的密码都是明文的,一旦数据库数据泄露,那就会造成不可估算的损失,所以我们通常都会使用非对称加密,简单理解也就是不可逆的加密,而 md5 加密算法就是符合这样的一种算法。
如 123456 用 Md5 加密后,得到的字符串:e10adc3949ba59abbe56e057f20f883e,就无法通过计算还原回 123456,我们把这个加密的字符串保存在数据库中,等下次用户登录时我们把密码通过同样的算法加密后再从数据库中取出这个字符串进行比较,就能够知道密码是否正确了,这样既保留了密码验证的功能又大大增加了安全性,但是问题是:虽然无法直接通过计算反推回密码,但是我们仍然可以通过计算一些简单的密码加密后的 Md5 值进行比较,推算出原来的密码
比如我的密码是 123456,你的密码也是,通过 md5 加密之后的字符串一致,所以你也就能知道我的密码了,如果我们把常用的一些密码都做 md5 加密得到一本字典,那么就可以得到相当一部分的人密码,这也就相当于“破解”了一样,所以其实也没有我们想象中的那么“安全”。
加盐 + 多次加密
既然相同的密码 md5 一样,那么我们就让我们的原始密码再加一个随机数,然后再进行 md5 加密,这个随机数就是我们说的盐(salt),这样处理下来就能得到不同的 Md5 值,当然我们需要把这个随机数盐也保存进数据库中,以便我们进行验证。
另外我们可以通过多次加密的方法,即使黑客通过一定的技术手段拿到了我们的密码 md5 值,但它并不知道我们到底加密了多少次,所以这也使得破解工作变得艰难。
在 Shiro 框架中,对于这样的操作提供了简单的代码实现:
String password = "123456";
String salt = new SecureRandomNumberGenerator().nextBytes().toString();
int times = 2; // 加密次数:2
String alogrithmName = "md5"; // 加密算法
String encodePassword = new SimpleHash(alogrithmName, password, salt, times).toString();
System.out.printf("原始密码是 %s , 盐是: %s, 运算次数是: %d, 运算出来的密文是:%s ",password,salt,times,encodePassword);
输出:
原始密码是 123456 , 盐是: f5GQZsuWjnL9z585JjLrbQ==, 运算次数是: 2, 运算出来的密文是:55fee80f73537cefd6b3c9a920993c25
SpringBoot 简单实例
通过上面的学习,我们现在来着手搭建一个简单的使用 Shiro 进行权限验证授权的一个简单系统
第一步:新建SpringBoot项目,搭建基础环境
pom包:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
application.properties文件:
#thymeleaf 配置
spring.thymeleaf.mode=HTML5
spring.thymeleaf.encoding=UTF-8
spring.thymeleaf.servlet.content-type=text/html
#缓存设置为false, 这样修改之后马上生效,便于调试
spring.thymeleaf.cache=false
#数据库
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/testdb?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.hbm2ddl.auto=update
#显示SQL语句
spring.jpa.show-sql=true
#不加下面这句则不会默认创建MyISAM引擎的数据库
spring.jpa.database-platform=org.hibernate.dialect.MySQL5InnoDBDialect
#自己重写的配置类,默认使用utf8编码
spring.jpa.properties.hibernate.dialect=com.wmyskxz.demo.shiro.config.MySQLConfig
第二步:新建实体类
新建一个【entity】包,在下面创建以下实体:
用户信息:
@Entity
public class UserInfo {
@Id
@GeneratedValue
private Long id; // 主键.
@Column(unique = true)
private String username; // 登录账户,唯一.
private String name; // 名称(匿名或真实姓名),用于UI显示
private String password; // 密码.
private String salt; // 加密密码的盐
@JsonIgnoreProperties(value = {"userInfos"})
@ManyToMany(fetch = FetchType.EAGER) // 立即从数据库中进行加载数据
@JoinTable(name = "SysUserRole", joinColumns = @JoinColumn(name = "uid"), inverseJoinColumns = @JoinColumn(name = "roleId"))
private List<SysRole> roles; // 一个用户具有多个角色
/** getter and setter */
}
角色信息:
@Entity
public class SysRole {
@Id
@GeneratedValue
private Long id; // 主键.
private String name; // 角色名称,如 admin/user
private String description; // 角色描述,用于UI显示
// 角色 -- 权限关系:多对多
@JsonIgnoreProperties(value = {"roles"})
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "permissionId")})
private List<SysPermission> permissions;
// 用户 -- 角色关系:多对多
@JsonIgnoreProperties(value = {"roles"})
@ManyToMany
@JoinTable(name = "SysUserRole", joinColumns = {@JoinColumn(name = "roleId")}, inverseJoinColumns = {@JoinColumn(name = "uid")})
private List<UserInfo> userInfos;// 一个角色对应多个用户
/** getter and setter */
}
权限信息:
@Entity
public class SysPermission {
@Id
@GeneratedValue
private Long id; // 主键.
private String name; // 权限名称,如 user:select
private String description; // 权限描述,用于UI显示
private String url; // 权限地址.
@JsonIgnoreProperties(value = {"permissions"})
@ManyToMany
@JoinTable(name = "SysRolePermission", joinColumns = {@JoinColumn(name = "permissionId")}, inverseJoinColumns = {@JoinColumn(name = "roleId")})
private List<SysRole> roles; // 一个权限可以被多个角色使用
/** getter and setter */
}
注意:这里有一个坑,还缠了我蛮久感觉,就是当我们想要使用RESTful风格返回给前台JSON数据的时候,这里有一个关于多对多无限循环的坑,比如当我们想要返回给前台一个用户信息时,由于一个用户拥有多个角色,一个角色又拥有多个权限,而权限跟角色也是多对多的关系,也就是造成了 查用户→查角色→查权限→查角色→查用户… 这样的无限循环,导致传输错误,所以我们根据这样的逻辑在每一个实体类返回JSON时使用了一个
@JsonIgnoreProperties
注解,来排除自己对自己无线引用的过程,也就是打断这样的无限循环。
根据以上的代码会自动生成user_info(用户信息表)、sys_role(角色表)、sys_permission(权限表)、sys_user_role(用户角色表)、sys_role_permission(角色权限表)这五张表,为了方便测试我们给这五张表插入一些初始化数据:
INSERT INTO `user_info` (`id`,`name`,`password`,`salt`,`username`) VALUES (1, '管理员','951cd60dec2104024949d2e0b2af45ae', 'xbNIxrQfn6COSYn1/GdloA==', 'wmyskxz');
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (1,'查询用户','userInfo:view','/userList');
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (2,'增加用户','userInfo:add','/userAdd');
INSERT INTO `sys_permission` (`id`,`description`,`name`,`url`) VALUES (3,'删除用户','userInfo:delete','/userDelete');
INSERT INTO `sys_role` (`id`,`description`,`name`) VALUES (1,'管理员','admin');
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (1,1);
INSERT INTO `sys_role_permission` (`permission_id`,`role_id`) VALUES (2,1);
INSERT INTO `sys_user_role` (`role_id`,`uid`) VALUES (1,1);
第三步:配置 Shiro
新建一个【config】包,在下面创建以下文件:
MySQLConfig:
public class MySQLConfig extends MySQL5InnoDBDialect {
@Override
public String getTableTypeString() {
return "ENGINE=InnoDB DEFAULT CHARSET=utf8";
}
}
这个文件关联的是配置文件中最后一个配置,是让 Hibernate 默认创建 InnoDB 引擎并默认使用 utf-8 编码
MyShiroRealm:
public class MyShiroRealm extends AuthorizingRealm {
@Resource
private UserInfoService userInfoService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 能进入这里说明用户已经通过验证了
UserInfo userInfo = (UserInfo) principalCollection.getPrimaryPrincipal();
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
for (SysRole role : userInfo.getRoles()) {
simpleAuthorizationInfo.addRole(role.getName());
for (SysPermission permission : role.getPermissions()) {
simpleAuthorizationInfo.addStringPermission(permission.getName());
}
}
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 获取用户输入的账户
String username = (String) authenticationToken.getPrincipal();
System.out.println(authenticationToken.getPrincipal());
// 通过username从数据库中查找 UserInfo 对象
// 实际项目中,这里可以根据实际情况做缓存,如果不做,Shiro自己也是有时间间隔机制,2分钟内不会重复执行该方法
UserInfo userInfo = userInfoService.findByUsername(username);
if (null == userInfo) {
return null;
}
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(
userInfo, // 用户名
userInfo.getPassword(), // 密码
ByteSource.Util.bytes(userInfo.getSalt()), // salt=username+salt
getName() // realm name
);
return simpleAuthenticationInfo;
}
}
自定义的 Realm ,方法跟上面的认证授权过程一致
ShiroConfig:
@Configuration
public class ShiroConfig {
@Bean
public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
System.out.println("ShiroConfiguration.shirFilter()");
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
// 拦截器.
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 配置不会被拦截的链接 顺序判断
filterChainDefinitionMap.put("/static/**", "anon");
// 配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了
filterChainDefinitionMap.put("/logout", "logout");
// <!-- 过滤链定义,从上向下顺序执行,一般将/**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;
// <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->
filterChainDefinitionMap.put("/**", "authc");
// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
shiroFilterFactoryBean.setLoginUrl("/login");
// 登录成功后要跳转的链接
shiroFilterFactoryBean.setSuccessUrl("/index");
//未授权界面;
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 凭证匹配器
* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了)
*
* @return
*/
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher() {
HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
hashedCredentialsMatcher.setHashAlgorithmName("md5"); // 散列算法:这里使用MD5算法;
hashedCredentialsMatcher.setHashIterations(2); // 散列的次数,比如散列两次,相当于 md5(md5(""));
return hashedCredentialsMatcher;
}
@Bean
public MyShiroRealm myShiroRealm() {
MyShiroRealm myShiroRealm = new MyShiroRealm();
myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
return myShiroRealm;
}
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
return securityManager;
}
/**
* 开启shiro aop注解支持.
* 使用代理方式;所以需要开启代码支持;
*
* @param securityManager
* @return
*/
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
return authorizationAttributeSourceAdvisor;
}
@Bean(name = "simpleMappingExceptionResolver")
public SimpleMappingExceptionResolver
createSimpleMappingExceptionResolver() {
SimpleMappingExceptionResolver r = new SimpleMappingExceptionResolver();
Properties mappings = new Properties();
mappings.setProperty("DatabaseException", "databaseError"); // 数据库异常处理
mappings.setProperty("UnauthorizedException", "403");
r.setExceptionMappings(mappings); // None by default
r.setDefaultErrorView("error"); // No default
r.setExceptionAttribute("ex"); // Default is "exception"
//r.setWarnLogCategory("example.MvcLogger"); // No default
return r;
}
}
Apache Shiro 的核心通过 Filter 来实现,就好像 SpringMvc 通过 DispachServlet 来主控制一样。 既然是使用 Filter 一般也就能猜到,是通过URL规则来进行过滤和权限校验,所以我们需要定义一系列关于URL的规则和访问权限。
Filter Chain定义说明:
- 1、一个URL可以配置多个Filter,使用逗号分隔
- 2、当设置多个过滤器时,全部验证通过,才视为通过
- 3、部分过滤器可指定参数,如perms,roles
Shiro内置的FilterChain,因表格显示问题,麻烦移步简书观看完整版..
- anon:所有url都都可以匿名访问
- authc: 需要认证才能进行访问
- user:配置记住我或认证通过可以访问
第四步:准备 DAO 层和 Service 层
新建【dao】包,在下面创建【UserInfoDao】接口:
public interface UserInfoDao extends JpaRepository<UserInfo, Long> {
/** 通过username查找用户信息*/
public UserInfo findByUsername(String username);
}
新建【service】包,创建【UserInfoService】接口:
public interface UserInfoService {
/** 通过username查找用户信息;*/
public UserInfo findByUsername(String username);
}
并在该包下再新建一个【impl】包,新建【UserInfoServiceImpl】实现类:
@Service
public class UserInfoServiceImpl implements UserInfoService {
@Resource
UserInfoDao userInfoDao;
@Override
public UserInfo findByUsername(String username) {
return userInfoDao.findByUsername(username);
}
}
第五步:controller层
新建【controller】包,然后在下面创建以下文件:
HomeController:
@Controller
public class HomeController {
@RequestMapping({"/","/index"})
public String index(){
return"/index";
}
@RequestMapping("/login")
public String login(HttpServletRequest request, Map<String, Object> map) throws Exception{
System.out.println("HomeController.login()");
// 登录失败从request中获取shiro处理的异常信息。
// shiroLoginFailure:就是shiro异常类的全类名.
String exception = (String) request.getAttribute("shiroLoginFailure");
System.out.println("exception=" + exception);
String msg = "";
if (exception != null) {
if (UnknownAccountException.class.getName().equals(exception)) {
System.out.println("UnknownAccountException -- > 账号不存在:");
msg = "UnknownAccountException -- > 账号不存在:";
} else if (IncorrectCredentialsException.class.getName().equals(exception)) {
System.out.println("IncorrectCredentialsException -- > 密码不正确:");
msg = "IncorrectCredentialsException -- > 密码不正确:";
} else if ("kaptchaValidateFailed".equals(exception)) {
System.out.println("kaptchaValidateFailed -- > 验证码错误");
msg = "kaptchaValidateFailed -- > 验证码错误";
} else {
msg = "else >> "+exception;
System.out.println("else -- >" + exception);
}
}
map.put("msg", msg);
// 此方法不处理登录成功,由shiro进行处理
return "/login";
}
@RequestMapping("/403")
public String unauthorizedRole(){
System.out.println("------没有权限-------");
return "403";
}
}
这里边的地址对应我们在设置 Shiro 时设置的地址
UserInfoController:
@RestController
public class UserInfoController {
@Resource
UserInfoService userInfoService;
/**
* 按username账户从数据库中取出用户信息
*
* @param username 账户
* @return
*/
@GetMapping("/userList")
@RequiresPermissions("userInfo:view") // 权限管理.
public UserInfo findUserInfoByUsername(@RequestParam String username) {
return userInfoService.findByUsername(username);
}
/**
* 简单模拟从数据库添加用户信息成功
*
* @return
*/
@PostMapping("/userAdd")
@RequiresPermissions("userInfo:add")
public String addUserInfo() {
return "addUserInfo success!";
}
/**
* 简单模拟从数据库删除用户成功
*
* @return
*/
@DeleteMapping("/userDelete")
@RequiresPermissions("userInfo:delete")
public String deleteUserInfo() {
return "deleteUserInfo success!";
}
}
第六步:准备页面
新建三个页面用来测试:
index.html:首页
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
index - 首页
</body>
</html>
login.html:登录页
<!DOCTYPE html>
<html xmlns:th="http://www.w3.org/1999/xhtml">
<head>
<meta charset="UTF-8">
<title>登录页</title>
</head>
<body>
错误信息:<h4 th:text="${msg}"></h4>
<form action="" method="post">
<p>账号:<input type="text" name="username" value="wmyskxz"/></p>
<p>密码:<input type="text" name="password" value="123456"/></p>
<p><input type="submit" value="登录"/></p>
</form>
</body>
</html>
403.html:没有权限的页面
<!DOCTYPE html>
<head>
<meta charset="UTF-8">
<title>403错误页</title>
</head>
<body>
错误页面
</body>
</html>
第七步:测试
- 编写好程序后就可以启动,首先访问
http://localhost:8080/userList?username=wmyskxz
页面,由于没有登录就会跳转到我们配置好的http://localhost:8080/login
页面。登陆之后就会看到正确返回的JSON数据,上面这些操作时候触发MyShiroRealm.doGetAuthenticationInfo()
这个方法,也就是登录认证的方法。 - 登录之后,我们还能访问
http://localhost:8080/userAdd
页面,因为我们在数据库中提前配置好了权限,能够看到正确返回的数据,但是我们访问http://localhost:8080/userDelete
时,就会返回错误页面.
注意:以上测试需要在REST工具中测