基于SSM的整合见文档
SSMS整合步骤.docx
一、Shiro简介
shiro是一个权限管理工具
java中常见的权限管理工具主要有:shiro、spring security、auth2等
Apache Shiro是一个功能强大且灵活的开源安全框架,可以清晰地处理身份验证,授权,企业会话管理和加密。
Apache Shiro的首要目标是易于使用和理解。安全有时可能非常复杂,甚至是痛苦的,但并非必须如此。框架应尽可能掩盖复杂性,并提供简洁直观的API,以简化开发人员确保其应用程序安全的工作。
以下是Apache Shiro可以做的一些事情:
- 验证用户以验证其身份
- 为用户执行访问控制,例如:
- 确定是否为用户分配了某个安全角色
- 确定是否允许用户执行某些操作
- 在任何环境中使用Session API,即使没有Web容器或EJB容器也是如此。
- 在身份验证,访问控制或会话生命周期内对事件做出反应。
- 聚合用户安全数据的1个或多个数据源,并将其全部显示为单个复合用户“视图”。
- 启用单点登录(SSO)功能
- 无需登录即可为用户关联启用“记住我”服务
……
以及更多 - 全部集成到一个易于使用的内聚API中。
Shiro尝试为所有应用程序环境实现这些目标 - 从最简单的命令行应用程序到最大的企业应用程序,而不强制依赖其他第三方框架,容器或应用程序服务器。当然,该项目旨在尽可能地融入这些环境,但它可以在任何环境中开箱即用。
二、Shiro功能
2.1 shiro功能介绍
Apache Shiro是一个具有许多功能的综合应用程序安全框架。下图显示了Shiro关注其能力的位置,此参考手册将以类似方式组织:
Authentication 身份认证/登录,验证用户是不是拥有相应的身份;
Authorization 授权,即权限验证,验证某个已认证的用户是否拥有某个权限;即判断用户是否能做事情,常
见的如:验证某个用户是否拥有某个角色。或者细粒度的验证某个用户对某个资源是否具有某个权限;
Session Management 会话管理,即用户登录后就是一次会话,在没有退出之前,它的所有信息都在会话中;会话可以是普通JavaSE环境的,也可以是如Web环境的;
Cryptography 加密,保护数据的安全性,如密码加密存储到数据库,而不是明文存储;
Web Support Web支持,可以非常容易的集成到Web环境;
Caching 缓存,比如用户登录后,其用户信息、拥有的角色/权限不必每次去查,这样可以提高效率;
Concurrency shiro支持多线程应用的并发验证,即如在一个线程中开启另一个线程,能把权限自动传播过去;
Testing 提供测试支持;
Run As 允许一个用户假装为另一个用户(如果他们允许)的身份进行访问;
Remember Me 记住我,这个是非常常见的功能,即一次登录后,下次再来的话不用登录了。
记住一点,Shiro不会去维护用户、维护权限;这些需要我们自己去设计/提供;然后通过相应的接口注入给Shiro即可。
2.2 shiro三大核心API
首先,从外部来看Shiro,即从应用程序角度的来观察如何使用Shiro完成工作。如下图:
三大核心:
可以看到:应用代码直接交互的对象是Subject,也就是说Shiro的对外API核心就是Subject;其每个API的含义:
Subject **主体,代表了当前“用户”,这个用户不一定是一个具体的人,与当前应用交互的任何东西都是
Subject,如网络爬虫,机器人等;即一个抽象概念;所有Subject都绑定到SecurityManager,与
Subject 的所有交互都会委托给SecurityManager,可以把Subject认为是一个门面; SecurityManager才是实际的执行者;
SecurityManager 安全管理器;即所有与安全有关的操作都会与SecurityManager交互;且它管理着所有Subject;
可以看出它是Shiro的核心,它负责与后边介绍的其他组件进行交互,如果学习过SpringMVC,你可以把它看成DispatcherServlet前端控制器;
Realm **域,安全数据源,Shiro从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用
户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm
得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource,即安全
数据源。
2.3 shiro内部架构
Subject 主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager 相当于SpringMVC中的DispatcherServlet;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator 认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实
现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authorizer 授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应
用中的哪些功能;
Realm 可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是
JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道
你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的
Realm;
SessionManager 如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,
这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普
通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体
与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;
接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可
以实现自己的分布式会话(如把数据放到Memcached服务器);
SessionDAO DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据
库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放
到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可
以使用Cache进行缓存,以提高性能;
CacheManager 缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,
放到缓存中后可以提高访问的性能
Cryptography 密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。
三、Shiro两大核心功能实现(认证+授权)
3.1 认证+授权
Authentication 认证
Authorization 授权
认证:
判断用户提交的账号密码是否合法
授权:
判断用户是否有权限进行相关操作
3.2 SpringBoot整合shiro
3.2.1 项目结构
3.2.2 User实体类
@Data
@Accessors(chain = true)
public class User {
private String account;
private String pwd;
}
3.2.3 pom.xml导入shior
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.0</version>
</dependency>
3.2.4 创建自己的Realm类
创建CustomRealm继承AuthorizingRealm类,该类中有两个方法,分别是获取授权信息和认证信息的方法
/**
* AuthorizingRealm 授权realm
*/
public class CustomRealm extends AuthorizingRealm {
private static final Logger logger = LoggerFactory.getLogger(CustomRealm.class);
/*
* 获取权限信息
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
return null;
}
/*
* 获取认证信息
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
String account = (String) token.getPrincipal();
String pwd = new String((char[])token.getCredentials());
User user = new User().setAccount("zhangsan").setPwd("123456");
if (user==null){
return null; //报账号不存在异常
}
SimpleAuthenticationInfo info =
new SimpleAuthenticationInfo(user.getAccount(),user.getPwd(),this.getName());
return info;
}
}
3.2.5 创建shiro配置类
创建shiro的配置类,配置realm、securitymanager和shiro过滤器
import com.woniuxy.shiro.realm.CustomRealm;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.LinkedHashMap;
import java.util.Map;
@Configuration //配置类标志
public class ShiroConfiguration {
private static final Logger logger = LoggerFactory.getLogger(ShiroConfiguration.class);
//1.配置realm:域对象,调用service获取用户的账号密码、权限信息 自定义
@Bean //<bean> 默认以方法名作为id:此处就是realm
public CustomRealm realm(){
logger.info("创建realm");
return new CustomRealm();
}
//2.安全管理器(核心)
@Bean //参数将相当于 <property ref = "realm">
public SecurityManager securityManager(CustomRealm realm){
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
//设置realm
manager.setRealm(realm);
//注入session的管理
manager.setSessionManager(sessionManager());
//
return manager;
}
//2.5步:shiro session的管理
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
// 去掉shiro登录时url里的JSESSIONID
sessionManager.setSessionIdUrlRewritingEnabled(false);
return sessionManager;
}
/**
* 3.shiro过滤器
* 主要来指定哪些请求需要认证、哪些不需要、哪些需要什么样的权限
*/
@Bean
public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager){
ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
//配置安全管理器
factoryBean.setSecurityManager(securityManager);
//配置没有权限时要请求的url(页面、后台url)
factoryBean.setUnauthorizedUrl("/html/error.html");//如果在后台跳 表明前后端不分离
//需要登录但是又没有登录时,自动跳转到指定的页面获取请求后台的url
factoryBean.setLoginUrl("/login.html");
//设置过滤器链
//HashMap 无序 LinkedHashMap 有序
Map<String,String> map = new LinkedHashMap<>();
//key:url value:标签shiro的过滤器
//anon是shiro的内置过滤器的名字,是anonymous单词前四个字母 表示能够匿名访问
//main.html
map.put("/login.html","anon");
map.put("/user/login","anon");
map.put("/user/regist","anon");
//静态资源放行
map.put("/html/*","anon");
map.put("/css/*","anon");
map.put("/js/*","anon");
map.put("/images/*","anon");
//注销操作
map.put("/user/logout","logout");
//除开以上情况以外的
//authc也是shiro内置过滤器的名字,是Authentication单词的缩写 表示需要认证(登录)之后才能访问
map.put("/**","authc");
//
factoryBean.setFilterChainDefinitionMap(map);
//
return factoryBean;
}
}
注:如果url上出现默认**JSESSIONID,则需要添加上述代码第2.5步:shiro session的管理,并在第二步中注入session的管理:manager.setSessionManager(sessionManager());
3.2.6 创建UserController处理请求
import com.woniuxy.shiro2.entity.User;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.crypto.hash.SimpleHash;
import org.apache.shiro.subject.Subject;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/user")
public class UserController {
@RequestMapping("/login")
public String login(User user){
//1.获取到“当前用户”对象
Subject currentUser = SecurityUtils.getSubject();
//2.判断用户是否认证过
if (!currentUser.isAuthenticated()){
//没有认证过 token 令牌
String pwd = user.getPwd();
UsernamePasswordToken token =
new UsernamePasswordToken(user.getAccount(),pwd);
try {
//当前用户携带令牌去认证,去找securitymanager进行认证
//认证时可能会报异常,只要出现异常说明令牌有问题,账号密码有问题
//不报任何异常表明账号密码没问题
currentUser.login(token);
//
return "认证成功";
}catch (UnknownAccountException e){
//账号不存在
e.printStackTrace();
//认证失败
return "账号不存在";
} catch (IncorrectCredentialsException e){
e.printStackTrace();
//密码错误
return "密码错误";
}
}
return "登录成功";
}
}
3.2.7 静态资源的编写与导入
- 在resources/static目录下创建js文件夹并导入jQuery
- 在resources/static目录下创建login.html
运行程序访问login.html进行登录操作,发现在realm的获取认证信息方法中打印到账号密码信息,表明shiro正常执行,该方法返回null表示没有找到任何账号信息,所以会报UnknownAccountException。<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="js/jquery.min.js"></script> <script> $(function () { $("#login").click(function () { let account = $("#account").val(); let pwd = $("#pwd").val(); $.ajax({ url:"user/login", type:"POST", data:{ account:account, pwd:pwd }, success:function (res) { console.log(res) } }); }); }); </script> </head> <body> <div> <input type="text" id="account"><br> <input type="password" id="pwd"><br> <button id="login">登录</button> </div> </body> </html>
四、Shiro认证流程
4.1 大致认证流程
4.2 详细认证流程
五、Shiro过滤器
shiro常见内置过滤器
- anon 匿名访问
- authc 需要认证
- logout 登出(注销)
浏览器发送请求时,所有的请求都会被shiro拦截,拦截之后shiro根据请求的URL与Map中的key进行从前往后的匹配,匹配到对应的key则获取到value中指定的过滤器名字,然后再根据过滤器的名字判断是否需要认证
解决没有认证情况下前端发送ajax请求后端将页面以JSON方式返回的问题:**
原因在于:ajax发完请求之后要求后台返回JSON数据,而shiro判断到当前用户没有认证,则以请求转发的方式返回login.html给浏览器,浏览器接收到数据,认为后台返回了数据,因此把页面当做是JSO那数据看待。
解决方案:
自定义一个controller,在controller中返回JSON数据,在过滤器链配置登录页面URL时填写controller相关方法的url
//配置没有权限时要请求的url(页面、后台url)
factoryBean.setUnauthorizedUrl("/jump/unauthorized");//如果在后台跳 表明前后端不分离
//需要登录但是又没有登录时,自动跳转到指定的页面获取请求后台的url
factoryBean.setLoginUrl("/jump/toLogin");
自定义controller
@RestController
@RequestMapping("/jump")
public class JumpController {
//去登录
@RequestMapping("/toLogin")
public ResponseResult<String> toLogin(){
ResponseResult<String> result = new ResponseResult<>();
result.setCode(500);
result.setData("TO_LOGIN");
result.setMessage("请登录");
return result;
}
//没权限
@RequestMapping("/unauthorized")
public ResponseResult<String> unauthorized(){
ResponseResult<String> result = new ResponseResult<>();
result.setCode(500);
result.setData("NO_UNAUTHORIZED");
result.setMessage("你没有权限");
return result;
}
}
六、Shiro加密模块
shiro加密模块是shiro提供的一个非常有用且重要的模块,其支持多种加密方式,可以将指定的明文加密成密文。
加密方式分类
- 对称加密:可以通过加密算法将明文加密成密文,也可以通过同样的加密算法将密文解密成明文
- 非对称加密:只能将明文加密成密文,无法将密文进行解密,常用用来对密码加密
shiro提供了SimpleHash类来加密明文
import org.apache.shiro.crypto.hash.SimpleHash;
public class CryTest {
public static void main(String[] args) {
//1.明文
String pwd = "123";
//2.加密得到密文 通过MD5加密方式将pwd加密得到32位的字符串
pwd = new SimpleHash("MD5",pwd).toString();
System.out.println(pwd);// 202cb962ac59075b964b07152d234b70
pwd = "123";
//参数4:加密次数
pwd = new SimpleHash("MD5",pwd,null,100).toString();
System.out.println(pwd); //156650ad747cfb25fcb9a7a2f8a02bdc
pwd = "123";
//参数3:盐值 盐值不能是随机
//盐一般用数据库中不会被修改的字段值作为盐 例如:id、账号、身份证
//假设数据库中有5个字段是不可能被修改的 盐值就可以用这5个字段排列组合 120
pwd = new SimpleHash("MD5",pwd,"zhangsan",100).toString();
System.out.println(pwd); //f391b473a0240b477164108ee71e68f8
}
}
注意:盐一般用数据库中不会被修改的字段值作为盐 例如:id、账号、身份证,不能是随机的
认证中使用密文**
在realm的获取认证信息方法中将User对象的pwd改为密文
User user = new User().setAccount("zhangsan").setPwd("f391b473a0240b477164108ee71e68f8");
由于MD5加密方式是非对称加密,因此在登录时应该将用户提交的明文通过同样的加密方式进行加密,然后再封装到token中
//没有认证过 token 令牌
String pwd = new SimpleHash("MD5",user.getPwd(),user.getAccount(),100).toString();
UsernamePasswordToken token = new UsernamePasswordToken(user.getAccount(),pwd);
运行项目测试
七、整合Mybatis,从数据库中获取账号密码
创建数据库
CREATE DATABASE shiro DEFAULT CHARACTER SET utf8;
USE shiro;
CREATE TABLE `user`(
id INT PRIMARY KEY AUTO_INCREMENT,
account VARCHAR(20),
pwd VARCHAR(32)
);
插入数据:此处的密码应该为密文(具体加密方式自定义,但是每次加密需要相同的方式)
创建UserMapper接口编写SQL语句
@Mapper
public interface UserMapper {
@Select("select * from user where account = #{account}")
public User findUserByAccount(String account);
}
在主启动类上扫描mapper
@SpringBootApplication
@MapperScan("com.woniuxy.shiro.mapper") //扫描mapper
public class ShiroApplication {
public static void main(String[] args) {
SpringApplication.run(ShiroApplication.class, args);
}
}
创建UserService
public interface UserService {
public User findUserByAccount(String account);
}
创建UserServiceImpl实现类
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserMapper userMapper;
@Override
public User findUserByAccount(String account) {
return userMapper.findUserByAccount(account);
}
}
在realm中注入UserService并修改获取认证信息方法
@Autowired
private UserService userService; //注入service对象
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//token中包含了账号密码信息
logger.info("调用realm的获取认证信息方法,得到对应用户的账号信息");
//获取账号、密码信息
String account = (String) token.getPrincipal();
String pwd = new String((char[])token.getCredentials());
logger.info(account+","+pwd);
//通过账号作为查询条件去数据库中查出对应的账号密码信息 调用service相关方法
//模拟假设获得了账号密码
//User user = new User().setAccount("zhangsan").setPwd("f391b473a0240b477164108ee71e68f8");
User user = userService.findUserByAccount(account);
if (user==null){
return null; //报账号不存在异常
}
//将查询到的结果封装到AuthenticationInfo中返回给securitymanager,securitymanager会根据
//AuthenticationInfo中的信息与token中的信息做比对 判断账号密码是否正确
//参数1:查询到的账号
//参数2:查询到的密码
//参数3:当前realm的名字
SimpleAuthenticationInfo info =
new SimpleAuthenticationInfo(user.getAccount(),user.getPwd(),this.getName());
return info;
}
重启项目进行测试
注意:在测试过程中如果出现不正常的情况,注销一下试试