前面的内容在这里
shiro框架的简单介绍以及使用(一)
shiro框架的密码校验(二)
shiro框架的权限设定(三)
这篇博客是根据前面几篇的代码来进行进阶的,循序渐进嘛
一、进阶背景
虽然我第二篇写了密码校验,但是那完全不够的,那个只是入门shiro,只是简单的了解一下流程。我们的都知道嗷,在开发环境下的登录模块,一般都是有三方登录的。
例如说优快云的登录它就有QQ登陆、APP登陆、微信登陆、短信登陆、账号密码登陆等等各种登陆方式。
如果开发环境下使用以我第二篇讲的那种方式实现登陆,那我只能说白给。
那这么多登陆方式,它是怎么实现的呢?如何去做一个扩展性强的设计呢?
ps:这种设计扩展性强不强我也不知道嗷,为了装犊子需要,shiro这玩意一个项目他也就写一次,写完了就不咋改了
我也就写过几次反正我以前的项目,只要是让我写shiro都是这种理念来实现的。
二、校验的理念与思路
我们要实现三方登陆,那肯定不止实现一个,这会有多种登陆方式。
那么多种登陆方式的话,我们使用shiro自带的UsernamePasswordToken实现类。
那是肯定不得行的,它不够力,因为我们不止需要传输账号密码,还得传输登陆方式。
那目前的思路就是:
- 做一个枚举类存储多种登陆方式
- 重写UsernamePasswordToken实现类
- 在AuthRealm中获取到登陆枚举
因为我们能在AuthRealm中获取到枚举的话,就已经可以通过枚举类型的不同来实现多种登陆方式了。
那我们先实现这部分思路
2.1LoginType枚举类
package com.lzl.practice.Util;
/**
* @program: practice
* @description: 登陆方式枚举
* @author: LiZelin
* @create: 2021-05-17 14:56
**/
public enum LoginType {
NORAML(1,true,"正常登陆"),
DX(2,false,"短信");
private LoginType(int code,boolean avoidPassword,String name){
this.code=code;
this.avoidPassword=avoidPassword;
this.name=name;
}
private int code;//编码
private boolean avoidPassword;//是否需要密码
private String name;//名称
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public boolean isAvoidPassword() {
return avoidPassword;
}
public void setAvoidPassword(boolean avoidPassword) {
this.avoidPassword = avoidPassword;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
首先写了一个登陆枚举类,定义了两种登陆方式,账号密码登陆,与短信登陆,因为微信登陆和QQ登陆的话,这个稍微麻烦一点,需要根据code值去查微信接口来实现,短信登陆简单一点,所以就先讲短信登陆,但是原理是一样的只是多加了一些东西。
2.2 重写userToken实现类
/**
* @program: practice
* @description: 重写userToken
* @author: LiZelin
* @create: 2021-05-17 16:20
**/
public class UserToenk extends UsernamePasswordToken {
private LoginType loginType;
public UserToenk(String user, String pass, LoginType loginType){
super(user,pass);
this.loginType=loginType;
}
public LoginType getLoginType() {
return loginType;
}
}
我们重写了一下UsernamePasswordToken类,将我们的loginType写了进去,这样我们在controller层传值的时候进行一下修改我们就可以在AuthRealm类里面获取到loginType的值了
2.3 修改AuthRealm类
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//AuthenticationToken转成UsernamePasswordToken这样可以直接拿到用户名和密码
UserToenk upToken = (UserToenk) token;
String username = upToken.getUsername();
//判断用户名是否为aaa否则抛出异常完事密码错误
if(!username.equals("aaa")){
throw new AuthenticationException("用户错误");
}
//测试在AuthRealm中是否能获取到LoginType的值,2为短信登陆
if (upToken.getLoginType().getCode()==2){
throw new UnknownAccountException("暂时不支持短信登陆");
}
//这个3.3的测试写的,刚刚忘记写了
check.encryption((HashedCredentialsMatcher)getCredentialsMatcher(),upToken.getPassword());
//把盐放进来
ByteSource salt=ByteSource.Util.bytes("6186808fa1ad5ba0ef39a88c97e900df");
return new SimpleAuthenticationInfo(upToken.getUsername(), "90a1626d55503a2c2e7eb345a4f3a607", salt,getName());
//new SimpleAuthenticationInfo(登录的用户名,数据库查的加密密码,盐值, Realm名可以直接写getName()随便叫啥没所谓)
}
我们在AuthRealm中写上获取LoginType的方法看是否能获取到LoginType的值,如果能获取到的话,我们就可以根据此登陆方式是否免密来进行验证登陆,到时候重写密码校验就完事了
2.4 修改Authcontroller类
/***
* @Description:
* @Param: [body]
* @return: java.lang.Object
* @Author: lizelinEGH|_
* @Date: 2021/5/13 0013 1:50
*/
@PostMapping("/login")
public Object login(@RequestBody String body) {
String username = JacksonUtil.parseString(body, "username");
String password = JacksonUtil.parseString(body, "password");
String Type = JacksonUtil.parseString(body, "type");
//根据传入的方式分配登陆枚举的类型,因为现在是讲思路嘛所以就简单写一下,实际开发不是这样写的
LoginType loginType;
if(Type.equals(LoginType.NORAML.getName())){
loginType=LoginType.NORAML;
}else loginType=LoginType.DX;
//获取Subject
Subject subject = SecurityUtils.getSubject();
//将loginType注入到Usertoenk中
UserToenk userToenk = new UserToenk(username,password,loginType);
try {
//登录测试,就这一行代码
subject.login(userToenk);
} catch (UnknownAccountException uae) {
return "暂未开发短信登陆";
} catch (LockedAccountException lae) {
return "用户帐号已锁定不可用";
} catch (AuthenticationException ae) {
return "用户名或密码不正确";
}
return "登录成功,账号名:"+username;
}
将LoginType 枚举注入到我们重写的UserToken实现类中,然后通过subject.login(userToenk);
将枚举传入到AuthRealm中
2.4 测试能不能获取到值
ok返回值没问题,暂未开发短信登陆,这说明枚举已经能在AuthRealm中获取到了,那我们能获取到那肯定就可以做出来两种不同的登陆方式。
三、多登陆方式的思路与实现
我们现在已经可以在AuthRealm中拿到登陆枚举了,也就是知道了当前的登陆方式。然后接下来需要做的就是,实现多种登陆的方式,然后根据当前登陆方式去匹配实现的登陆方式,也就是多态实现。
那接下来的思路就是:
- 写一个多种登陆方式的接口
- 实现短信登陆与账号密码登陆
- 重写HashedCredentialsMatcher类的校验方法
- 然后在AuthRealm中将当前登陆方式与实现登陆方式匹配起来
只要能匹配起来了,就可以说短信登陆+账号密码登陆就已经搞定了。
当然我是模拟短信登陆嗷,主要讲的是思路,不是自己给你代码嗷,短信验证码它真心不难。
当然你要是真不会写,那我也就只能在写一篇博客了 =-=。
如果我们使用多登陆方式接口的话那么就不能使用HashedCredentialsMatcher的默认密码验证了
需要我们自己写一个,所以ShiroConfig里面的HashedCredentialsMatcher注入就需要更改。
3.1 修改controller层
/***
* @Description:
* @Param: [body]
* @return: java.lang.Object
* @Author: lizelinEGH|_
* @Date: 2021/5/13 0013 1:50
*/
@PostMapping("/login")
public Object login(@RequestBody String body) {
String username = JacksonUtil.parseString(body, "username");
String password = JacksonUtil.parseString(body, "password");
Integer code = JacksonUtil.parseInteger(body, "code");
//根据传入的方式分配登陆枚举的类型,因为现在是讲思路嘛所以就简单写一下
LoginType loginType;
if(code == LoginType.NORAML.getCode()){
loginType=LoginType.NORAML;
}else {
loginType=LoginType.DX;
}
//获取Subject
Subject subject = SecurityUtils.getSubject();
//将loginType注入到Usertoenk中
UserToken usertoken = new UserToken(username,password,loginType);
try {
//登录测试,就这一行代码
subject.login(usertoken);
} catch (UnknownAccountException uae) {
return "暂未开发短信登陆";
} catch (LockedAccountException lae) {
return "用户帐号已锁定不可用";
} catch (AuthenticationException ae) {
return "用户名或密码不正确";
}
return "登录成功,账号名:"+username;
}
将前台传输的登录类型换成code,int类型
两种登录方式识别已经改了,那么接下来我们就需要重写一下HashedCredentialsMatcher类的密码校验方法。
因为账号密码登录的话前端是会给你传值密码的。
但是短信登录他是免密登录,前台是只会传账号、验证码、登录方式的。
所以我们需要将HashedCredentialsMatcher类的密码校验方法,
修改成为如果短信登录那就免密,账号密码登录那就是以前的登录方式。
3.2 修改CheckUtil类
/***
* @Description: 密码校验
* @Param:
* @return:
* @Author: lizelin
* @Date: 2021/5/14 0014 18:32
*/
@Component
public class CheckUtil extends HashedCredentialsMatcher{
//获得密文
public String encryption(HashedCredentialsMatcher Hashed, char[] passWord){
System.out.println("开始");
String random = new SecureRandomNumberGenerator().nextBytes().toHex();//获得随机数盐值
System.out.println(random);//打印盐值
ByteSource salt=ByteSource.Util.bytes(random);//将盐值转为ByteSource类型
System.out.println();
//获得加密后的密码
//SimpleHash hash = new SimpleHash("HashedCredentialsMatcher配置的加密方式","明文密码" , "盐值", "加密次数");
SimpleHash hash = new SimpleHash(Hashed.getHashAlgorithmName(), passWord, salt, Hashed.getHashIterations());
String encodedPassword = hash.toHex();
//打印加密后的密文
System.out.println(encodedPassword);
return "";
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
//获取存到info里面的salt,如果它为空那也就代表为免密登录
// 具体看AuthRealm的实现代码就可以了,因为我是这样做的
Object salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt();
if (salt==null){
return true;
}
Object tokenHashedCredentials = hashProvidedCredentials(token, info);
Object accountCredentials = getCredentials(info);
return equals(tokenHashedCredentials, accountCredentials);
}
}
让CheckUtil类继承HashedCredentialsMatcher重写doCredentialsMatch方法。
在里面判断salt的值,为空则是免密登录,不为空就正常登录就完事了。
3.3 修改AuthRealm类
@Autowired
CheckUtil check;
@Autowired
UserInfo userInfo;
/***
* @Description: 这块是登录验证配置
* @Param: [token]
* @return: org.apache.shiro.authc.AuthenticationInfo
* @Author: lizelin
* @Date: 2021/5/13 0013 1:25
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
//AuthenticationToken转成UsernamePasswordToken这样可以直接拿到用户名和密码
UserToken upToken = (UserToken) token;
String username = upToken.getUsername();
//获取多态的LoginWay
LoginWay loginWay = upToken.getLoginWay();
//获取账号密码信息,这里我直接模拟数据了,本来应该链接数据库的
//拿username去查询数据库就完事了
userInfo.setUser("aaa");
userInfo.setPass("90a1626d55503a2c2e7eb345a4f3a607");
userInfo.setSalt("6186808fa1ad5ba0ef39a88c97e900df");
ByteSource salt=ByteSource.Util.bytes(userInfo.getSalt());
//判断用户名是否为aaa否则抛出异常完事密码错误
if(!username.equals("aaa")){
throw new AuthenticationException("用户错误");
}
if(loginWay.isAvoidPassword()){
return new SimpleAuthenticationInfo(upToken.getUsername(), userInfo.getPass(),getName());
}else {
return new SimpleAuthenticationInfo(upToken.getUsername(), userInfo.getPass(), salt,getName());
}
}
将Authrealm类的doGetAuthenticationInfo方法修改为这样就ok了,我是模拟的数据库,但是实际使用情况的话只需要将模拟数据库代码改为链接数据库就完事了
ps:话说不会真的有人不会查询数据库吧,不会真的这个代码都写不明白吧,不会吧不会吧
然后下一步就简单了,将ShiroConfig里面的注入HashedCredentialsMatcher改成注入我们重写的类就完事了
3.4 修改AuthRealm类
//将自己的验证方式加入容器
@Bean
public Realm AuthRealm() {
AuthRealm authRealm = new AuthRealm();
CheckUtil credentials= new CheckUtil();
credentials.setHashIterations(3);//加密次数
credentials.setHashAlgorithmName("MD5");//加密方式
authRealm.setCredentialsMatcher(credentials);//注入到AuthRealm实现类中
return authRealm;
}
将这个方法改一下就完事了,换一个注入的类
然后我们直接去测试就好了,因为controller层的代码没有变
四、测试代码
4.1 免密登录测试
这里可以看到我们没有输入密码,依然登录成功了,因为短信这个枚举做的是免密登录
4.2 正常登录测试
正常登录的话测试还是没问题的
上面这些截图的时候,我的controller层的登录类型传输的时候没有改成code,String类型识别有时候会出错!!!
这里的话关于验证密码这,这个实际上当时我在写shiro的时候呢,整个密码校验、加密这些都是没有借助CredentialsMatcher接口的,因为当时我做的时候采用的是BCryptPasswordEncoder加密。
但是我今天写的这个博客采用的是重写HashedCredentialsMatcher类,因为这个比较方便写。
而且总体来说思路是一样的,只是实现方式不同,学会了思路,自己就能写了咯。
五、后续思路
这篇已经够长了,就不继续往下写了5.1 关于短信验证
短信验证的话,其实蛮简单的
- 首先controller层里面,写一个发验证码的方法提交到三方短信服务器
- 然后将发送的验证码存储到CaptchaCodeManager缓存里面
- 然后到他登录的时候不是得把验证码传过来的吗,判断有没有过期就完事了
5.2 关于扫码登录
微信扫码登录和QQ扫码登录其实也是差不多的
你配置好了app环境
微信扫码的时候会有一个code值,前台传给你之后
String token= HttpClientUtil.get(GETTOKEN.replaceAll("CODE", code2));
然后直接就是HttpClientUtil去调用微信接口GETTOKEN,他就给你返回令牌和openid
userInfo = HttpClientUtil.get(USERINFO.replaceAll("TOKEN", access_token).replaceAll("OPENID", openid));
然后去调用USERINFO地址,就能获得微信用户信息了,就这么简单,说白了就是调三方接口。
5.3 不知道取什么名字
这个登录的话,除了要调用接口的方式不同,说白了就分两种。
一种是免密,一种是常规登录
因为登录方式不同的话调用的三方接口也是不同的,你每种方式写一个方法
然后判断一下前台传过来的登录类型,调用不同的方法就完事了
然后密码校验的地方呢,刚刚也说了就分两种,常规和免密,这两个你做到了,后面无论加上什么登录方式也都只是调用不同的三方接口而已,没什么区别。
多登录它就是这么简单。