1.Shiro认证
1.1 Shiro过滤器(拦截非法访问)
》anon代表不认证也可以访问的资源,比如静态资源,还有登录页面
》authc代表必须通过认证才可以访问,通常对动态资源(controller,jsp页面)进行拦截,如果用户没有认证,Shiro会自动跳转到login.jsp页面
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!--注入SecurityManager-->
<property name="securityManager" ref="securityManager"/>
<property name="filterChainDefinitions">
<value>
<!-- 对静态资源进行放行 任何人都可以访问-->
/css/**=anon
/img/**=anon
/plugins/**=anon
<!-- 任何人都可以登录,所以login-shiro.do也是匿名-->
/system/user/login-shiro.do=anon
<!-- authc标记的资源,只有登录认证之后才能访问 也是看session中是否有user-->
/**=authc
</value>
</property>
<!--如果 session就是没有user,表示未登录,页面转到login-shiro.jsp -->
<property name="loginUrl" value="/login-shiro.jsp"/>
</bean>
1.2用户密码判断
没有使用判断语句而是try/catch
使用shiro框架进行认证 结果也是三种可能 正确 没有异常,
用户不存在 UnknownAccountException,
密码出错 IncorrectCredentialsException
图解
UserController
//${path}/system/user/login-shiro.do
@RequestMapping(path = "/login-shiro", method = {RequestMethod.GET, RequestMethod.POST})
public String loginShiro(String email,String password) {
//根据 email查询对应的用户
l.info("loginShiro email " + email);
l.info("loginShiro password " + password);
//使用shiro框架进行认证 结果也是三种可能 正确 没有异常,用户不存在 UnknownAccountException,密码出错 IncorrectCredentialsException
//本质是需要调用realm进行查找用户
//1:先获取subject 表示对securitymanager连接
Subject subject = SecurityUtils.getSubject();//获取连接
//2:调用 securitymanager
UsernamePasswordToken token = new UsernamePasswordToken(email, password);//身份验证
try {
//3:再调用realm
subject.login(token);//正确 --realm
//正确
l.info("正确");
//保存用户信息
//要求访问realm返回一个user对象
User user = (User) subject.getPrincipal();// --realm
session.setAttribute("loginUser",user);
//一个 Module对象 就是左侧栏上的一个菜单项
List<Module> menus = iModuleService.findModulesByUser(user);
session.setAttribute("menus",menus);
l.info("login menus "+menus);
//跳到主页
return "redirect:/home/toMain.do";
} catch (UnknownAccountException e) {//用户不存在
e.printStackTrace();
l.info("用户不存在");
request.setAttribute("error","用户不存在");
return "forward:/login-shiro.jsp";
}catch (IncorrectCredentialsException e){//密码出错
e.printStackTrace();
l.info("密码不对");
request.setAttribute("error","邮箱或者密码不对");
return "forward:/login-shiro.jsp";
}
}
AuthRealm
//认证(登录账号密码)
//subject.login(token);
//参1 接受 subject.login(token); 方法的值 authenticationToken
@Autowired
IUserService userService;
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
l.info("AuthRealm doGetAuthenticationInfo 函数执行了");
//响应login方法和getPrincipal方法的调用
UsernamePasswordToken usernamePasswordToken= (UsernamePasswordToken) token;
String email = usernamePasswordToken.getUsername();
l.info("doGetAuthenticationInfo --"+email);
//调service读dao
User user = userService.findUserByEmail(email);
l.info("doGetAuthenticationInfo --"+user);
if (user == null) {
//用户不存在
return null;//-->系统会将null转成UnknownAccountException抛出
} else {
//用户存在的
//AuthenticationInfo 返回给 User user = (User) subject.getPrincipal();
/**
* 参数一:principal,存放用户登录信息,subject.getPrincipal()获取
* 参数二:数据库的密码
* 参数三:realm的别名,只有在多个Realm的时候才会用,一般不用
*/
AuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), "");
return info;
}
}
2.加密/加盐加密
2.1MD5加密工具类MD5Utils
md5的实现
public class MD5Utils {
//
public static String stringToMD5(String plainText) {//参1 明文
byte[] secretBytes = null;
try {
secretBytes = MessageDigest.getInstance("md5").digest(
plainText.getBytes());//对明文的字节进行摘要
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException("没有这个md5算法!");
}
//16或者32
String md5code = new BigInteger(1, secretBytes).toString(16);
//补0 一个字节转两位的16进制
for (int i = 0; i < 32 - md5code.length(); i++) {
md5code = "0" + md5code;
}
return md5code;
}
}
TestMd5Util
public class TestMd5Util{
//测试Md5
@Test
public void stringToMD5() {
String result = MD5Utils.stringToMD5("123");
System.out.println(result);
//md5(123)= 202cb962ac59075b964b07152d234b70
//202cb962ac59075b964b07152d234b70
}
}
Shiro框架也集成了常用的加密的算法md5
Md5Hash对象
@Test
public void test02() {
Md5Hash md5Hash = new Md5Hash("123");//参1 传入明文
System.out.println(md5Hash.toString());
//202cb962ac59075b964b07152d234b70
}
2.2加盐(除了密码多了一个字符串,防止被别人利用彩虹表撞库来破解密码)
shiro加盐加密步骤
1、编写自定义凭证匹配器
2、在applicationContext-shiro.xml,添加自定义凭证匹配器
2.3.1 配置自定义的密码匹配器
CustomCredentialsMatcher
把表单获取的数据转为密文,再与数据库的取出的数据进行比较
/**创建自己的密码匹配器 - 通过加盐加密对用户输入的明文进行加密然后和数据库中存储的密文比较
* 必须指定父类SimpleCredentialsMatcher
*/
public class CustomCredentialsMatcher extends SimpleCredentialsMatcher {
@Autowired
IUserService userService;
private Logger l = LoggerFactory.getLogger(this.getClass());
//将密码加密成密文,但需要使用账号(这里账号是邮箱)作盐
//subject.login(token) 获取页面提交的表单数据 通过加盐加密转为密文
//info 调relam 查询数据库中的密码(数据库中的密码存储就是加密存储的) -> 密文2
//通过比较密文1(用户输入的值)和密文2(数据库中的值),来判断用户输入是否正确
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//token获取账号密码
UsernamePasswordToken t = (UsernamePasswordToken) token;
String email = t.getUsername(); //获取用户名(邮箱)
//密码是char[] '1','2','3' => "123" new String(char[])
String pwd1 = new String(t.getPassword()); //密码获取的是字符数组,转换为字符串
l.info("doCredentialsMatch 用户输入的密码明文pwd1 = "+pwd1);
//进行加盐加密,得到用户输入的密码明文加密后的密文
Md5Hash md5Hash = new Md5Hash(pwd1, email);//参1 传入明文 参2盐
pwd1 = md5Hash.toString(); //转换为字符串
l.info("doCredentialsMatch 用户输入的密码加盐加密后pwd1 "+pwd1);
//读取数据库的密码,数据库存储的就是密文
String pwd2 = (String) info.getCredentials();
l.info("doCredentialsMatch 数据库的密码密文pwd2 = "+pwd2);
if (pwd1.equals(pwd2)) {
return true;//密码正确(两个密文比较)
} else {
return false;//密码不正确
}
}
2.3.2在applicationContext-shiro.xml添加自定义的凭证匹配器(自定义密码匹配)
<!--使用自己定义的匹配器-->
<bean id="credentialsMatcher2" class="com.zx.web.utils.CustomCredentialsMatcher"></bean>
<!-- 3.创建Realm(类似dao),在AuthRealm类上就不需要加@Component注解了 -->
<bean id="authRealm" class="com.zx.web.shiro.AuthRealm">
<!-- 使用自定义匹配器(密码) -->
<property name="credentialsMatcher" ref="credentialsMatcher2"/>
</bean>
2.3.3修复添加用户时对密码加盐加密
UserServiceImpl
@Override
public void saveUser(User user) {
String uuid= UUID.randomUUID().toString();
user.setUserId(uuid);
//原来保存用户使用的密码是明文,现在需要对它进行加密
if(user.getPassword()!=null){
Md5Hash md5Hash = new Md5Hash(user.getPassword(),user.getEmail());//参1 明文 参2 盐
user.setPassword(md5Hash.toString());
}
l.info("saveUser "+user);
iUserDao.save(user);
}
3.会话管理(退出功能)
退出功能就是:退出后删除session中的数据并销毁session
3.1普通退出
@RequestMapping(path = "/loginOut-shiro", method = {RequestMethod.GET, RequestMethod.POST})
public String LoginOut(){
//删除session中的用户信息
session.removeAttribute("loginUser");
//销毁session
session.invalidate();
return "redirect:/login.jsp";
}
3.2 Shiro退出
删除Shiro的session(底层:删除session数据)
Subject subject = SecurityUtils.getSubject();
subject.logout();
@RequestMapping(path = "/loginOut-shiro", method = {RequestMethod.GET, RequestMethod.POST})
public String loginOutShiro(){
//删除session中的用户信息
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "redirect:/login-shiro.jsp";
}
4.授权访问校验(防止已经登录的用户通过链接访问不属于自己的权限)
登陆认证后,系统校验用户是否有权限访问资源,就叫授权
4.2步骤
1 登陆认证成功后,获取用户的权限 (获取权限)
2 访问资源时候,进行授权校验:用访问资源需要的权限去用户权限列表查找,如果存在,则有权限访问资源。(权限拦截)
4.3方法
授权校验有几种方式
1) 硬编码方式(拦截方法)(非Web应用,Web应用)
2) 过滤器配置方式(拦截url)(Web应用)
3) 注解方式(拦截方法)(Web应用)
4) shiro提供的标签((拦截页面元素:按钮,表格等))(Web应用)
4.4获取权限
4.4.1查询权限sql
根据用户的登录时表单传来的邮箱进行查询用户的权限名
用到四张表 用户表 用户角色表 角色权限表 权限表
select distinct m.name from pe_user u
inner join pe_role_user ru on ru.user_id = u.user_id
inner join pe_role_module rm on ru.role_id = rm.role_id
inner join ss_module m on m.module_id = rm.module_id
where u.email = 'lw@export.com'
4.4.2编写 查询权限的逻辑
//授权(查有什么权限)
@Autowired
IModuleService iModuleService;
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
l.info("AuthRealm doGetAuthorizationInfo 函数执行了");
//获取用户信息
Subject subject = SecurityUtils.getSubject();
User user = (User) subject.getPrincipal();//session
l.info("doGetAuthorizationInfo user "+user);//当前用户是谁
if(user != null){
//要求查询当前账号email包含哪些权限(中文表示)
List<Module> list = iModuleService.findModulesByUser(user);
SimpleAuthorizationInfo info =new SimpleAuthorizationInfo();
//循环
for(Module m:list){
l.info("doGetAuthorizationInfo m "+m.getName());
//将字符串表示的权限名称添加到shiro
info.addStringPermission( m.getName());
}//end for
return info;
}//end if
return null;
}
4.5具体的方法
4.5.1访问代码 硬编码(不推荐)
在每一需要对应权限才能访问的功能方法中写出所需要的权限(用户没有这个权限就不能访问)
写在Controller的功能方法中
例子:如果该用户没有企业管理权限则无法通过链接直接访问该功能
Subject subject = SecurityUtils.getSubject();
subject.checkPermission("企业管理");
4.5.2XML配置方式实现权限管理(推荐)
1)原理:shiro过滤器
拦截请求时,获取授权
2)在applicationContext-shiro.xml
toList.do方法只有用户拥有企业管理或者用户管理权限时才能访问
<!-- 给url配置权限-->
/company/toList.do=perms["企业管理"]
/system/user/toList.do=perms["用户管理"]
3)可以指定 无权限时的页面提示
4.5.3shiro授权 注解方式实现
4.5.4shiro授权 注解方式实现
1 在applicationContext-shiro.xml中开启shiro注解支持
<!-- @RequiredPermission-->
<bean id="lifecycleBeanPostProcessor"
class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
<bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator"
depends-on="lifecycleBeanPostProcessor"/>
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor">
<property name="securityManager" ref="securityManager"/>
</bean>
2.开启Aop自动代理
<aop:aspectj-autoproxy/>
3.在controller中的方法上使用
@RequiresPermissions("企业管理")
4.3.5shiro授权 Shiro标签实现权限管理
用在jsp页面
1 在页面引入shiro标签 ,类似c标签
<%@ taglib prefix=“shiro” uri=“http://shiro.apache.org/tags” %>
2 使用shiro标签 给name赋值,赋上什么值就表示需要什么权限
标签会查询权限,如果有权限,就对应的权限页面显示,如果没有权限不报错,将页面内容进行隐藏
<%-- 标签会查询权限,如果没有权限不报错,将页面内容进行隐藏,反之显示页面内容--%>
<shiro:hasPermission name="企业管理">
<a href="">企业管理</a>
</shiro:hasPermission>
<shiro:hasPermission name="用户管理">
<a href="">用户管理</a>
</shiro:hasPermission>
<shiro:hasPermission name="日志管理">
<a href="">日志管理</a>
</shiro:hasPermission>
5.小结
无论是认证还是授权,都是要先进行连接,才能对安全管理器进行访问,并且通过安全管理器来访问realm
Subject subject = SecurityUtils.getSubject();