一、guns中实现认证登陆的原理
guns中的认证登陆系统是源自shrio框架,与其的区别只不过guns框架的作者已经把这个框架整合进来了,所以和网上找到的shrio配置原理相似,但是配置的地方不一样。
首先,在shrio中有三大组件,分别是:
Subject :正与系统进行交互的人,或某一个第三方服务。所有 Subject 实例都被绑定到(且这是必须的)一个SecurityManager 上。
SecurityManager:Shiro 架构的心脏,用来协调内部各安全组件,管理内部组件实例,并通过它来提供安全管理的各种服务。当 Shiro 与一个 Subject 进行交互时,实质上是幕后的 SecurityManager 处理所有繁重的 Subject 安全操作。
Realms :本质上是一个特定安全的 DAO。当配置 Shiro 时,必须指定至少一个 Realm 用来进行身份验证和/或授权。Shiro 提供了多种可用的 Realms 来获取安全相关的数据。如关系数据库(JDBC),INI 及属性文件等。可以定义自己 Realm 实现来代表自定义的数据源。
我们认证可以理解为,人即为Subject,securityManager就是我们程序的安全管家,而他确定要不要放人过去的花名册就是我们配置好的Realms。
二、guns中的配置文件分布
首先是类似传统xml存在的配置类ShiroConfig,其位于cn.stylefeng.guns.config.web包下,在其中配置了DefaultWebSecurityManager类,也就是安全管理器,可以说这就是花名册,目前我主要用到他的Realm设置和设置执行所有身份验证操作的委托身份验证器(就是具体条目和给条目排版的目录)。除此之外其中还有多机环境与单机环境的配置文件,以及默认登陆跳转,又或者目录权限管理等杂七杂八的功能。
其次是guns作者给我们写好的一个shrio实例,即默认User用户的实例,其放在cn.stylefeng.guns.core.shiro下
其主要是由两个业务类,一个工具类,一个shiro专用模型,一个User表的Realm所组成。也就是说我们要临摹的话,必须有shiro专业模型,对应业务类,对应Realm表。当然除此之外为了实现Shiro的多Realm支持还需要弄一些东西,这个等下说。
ShiroDbRealm继承于AuthorizingRealm,为此他需要实现两个方法,分别是:
除此之外,还有一个设置加密方式的方法,不过这不是这次重点,先不提。
我们主要看登陆认证,这边他先声明了一个service服务类,区别在于他是在service内部用spring方法请求出来的,而我们大多时候是@Autowired来请求。
然后他把传来的数据转换为了UsernamePasswordToken,这是个Shrio包自带的数据类,里面存着账号密码等信息。
第三四行调用了User方法将token对应数据库的结果查询出来,并放到Shrio专用的类里面。
最后一行依旧是在调用service的方法,将判断数据是否正确放到了service处理。这里可以看出和网上的代码不同,这一部分他都封装到service里面了,所以我们主要应该看service。
在Service中的user方法其实就是查询数据库,并对基础错误进行判断,然后返回数据。
shiroUser里面主要干的呢就是读取用户权限列表,并保存在shiro对象里面传出去。
info中大部分数据都是照抄,比较重要的就是Salt,加盐的Md5加密。设置随机数密钥之后,密码就变得很难解开,然后将这四个数据塞入Shrio封装的类里面,realm干的事情就完成了。(其实权限这块我还不太明白,不过不影响现在的使用,先凑合着用吧)
然后ShiroKit类也没什么好说的,只是一个单纯的工具类而已。
三、实现多Realm需要进行的更改
要实现多Realm,需要先重写UsernamePasswordToken类,为什么呢?是因为原来的类有账号密码这类东西,可是并没有说明这个Token是属于A类用户还是属于B类用户,为了让其有这个功能,我们就要先手动给他带上这个东西。
public class CustomLoginToken extends UsernamePasswordToken {
private static final long serialVersionUID = 1L;
private String loginType;
public CustomLoginToken(final String username, final String password, String loginType){
super(username, password);
this.loginType = loginType;
}
public String getLoginType() {
return loginType;
}
public void setLoginType(String loginType) {
this.loginType = loginType;
}
}
类中继承了原来的Token,并且在这基础上多加了一个loginType属性,并要求初始化这个类时候必须多加一个声明这个token对照哪个表的参数。
当然显然你写好的这个token,人家shrio是识别不了的,为了能圆谎,那就得接着把使用这个token的类也重写一次。
这里主要有三个地方要记得重写,首先第一个就是ModularRealmAuthenticator类,这个类就可以理解成花名册的目录,shrio用哪个realm来检测token,就是靠这个目录来索引的。所以我们先把目录给重写了:
public class CustomizedModularRealmAuthenticator extends ModularRealmAuthenticator {
@Override
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken)
throws AuthenticationException {
// 判断getRealms()是否返回为空
assertRealmsConfigured();
// 强制转换回自定义的CustomizedToken
CustomLoginToken customizedToken = (CustomLoginToken) authenticationToken;
// 登录类型
String loginType = customizedToken.getLoginType();
System.out.println("申请登录的类型:"+loginType);
// 所有Realm
Collection<Realm> realms = getRealms();
// 登录类型对应的所有Realm
Collection<Realm> typeRealms = new ArrayList<>();
for (Realm realm : realms) {
if (realm.getName().contains(loginType)) {
System.out.println("判断成功,添加"+realm.getName()+"表");
typeRealms.add(realm);
}
}
// 判断是单Realm还是多Realm
if (typeRealms.size() == 1)
return doSingleRealmAuthentication(typeRealms.iterator().next(), customizedToken);
else
return doMultiRealmAuthentication(typeRealms, customizedToken);
}
}
这里需要注意的事情是,这里的realms是源自你在ShiroConfig中给花名册塞的realm,这个我们等下就会说到,然后这里是怎么判断你要用哪个realm呢?靠的是你写进loginType的信息是否有在realm的类名中出现,有的话就会塞入集合中。
但是光这样也还是不够,你还需要把这个判断类加入到shrio的代码体系里面,这就要修改ShiroConfig类了,让她变成我们需要的样子。
/**
* 安全管理器
*/
@Bean
public DefaultWebSecurityManager securityManager(CookieRememberMeManager rememberMeManager, CacheManager cacheShiroManager, SessionManager sessionManager) {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
List<Realm> list = new ArrayList<Realm>();
securityManager.setAuthenticator(authenticator());
list.add(this.shiroDbRealm());
securityManager.setRealms(list);
securityManager.setCacheManager(cacheShiroManager);
securityManager.setRememberMeManager(rememberMeManager);
securityManager.setSessionManager(sessionManager);
return securityManager;
}
@Bean
public CustomizedModularRealmAuthenticator authenticator() {
CustomizedModularRealmAuthenticator authenticator = new CustomizedModularRealmAuthenticator();
authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());
return authenticator;
}
这里对源代码进行了更改,首先给securityManager的authenticator属性塞入了一个值,而使用的方法则是springboot常用的@bean声明方式,将我们写好的分类器加入了大家庭中。第二步呢则是把原来的setRealm改成了serRealms,这样我们就可以把Realm集成一个list,一起传过去,告诉她我们的花名册上面总共有哪些东西。
在做完这些后还没完,在拦截器上面,shrio也有动手脚,为了避免在拦截器中出问题,我们需要修改一下AttributeSetInteceptor的代码:
public class AttributeSetInteceptor extends HandlerInterceptorAdapter {
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
//没有视图的直接跳过过滤器
if (modelAndView == null || modelAndView.getViewName() == null) {
return;
}
//视图结尾不是html的直接跳过
if (!modelAndView.getViewName().endsWith("html")) {
return;
}
if(modelAndView.getViewName().indexOf("/modular/agentLogin/")!=-1) {
System.out.println("接受代理登陆请求");
return;
}
Object user;
if(modelAndView.getViewName().indexOf("/modular/agent/")!=-1) {
user = ShiroKit.getDl();
}else {
user = ShiroKit.getUser();
}
if (user == null) {
throw new AuthenticationException("当前没有登录账号!");
} else if(user instanceof ShiroUser){
ShiroUser linshi = (ShiroUser)user;
modelAndView.addObject("name", linshi.getName());
modelAndView.addObject("avatar", DefaultImages.defaultAvatarUrl());
modelAndView.addObject("email", linshi.getEmail());
}else if(user instanceof ShiroDl){
ShiroDl linshi = (ShiroDl)user;
modelAndView.addObject("tel", linshi.getTel());
modelAndView.addObject("password", linshi.getPassword());
modelAndView.addObject("agentId", linshi.getAgentId());
modelAndView.addObject("gameId", linshi.getGameId());
}else {
throw new AuthenticationException("数据判断异常");
}
}
}
这里为了演示怎么写两个Realm的拦截器,提前把ShiroDl加入了进来,可以看到为了避免拦截器在获得请求之后直接调用ShiroKit.getUser方法,导致强制转换崩溃,所以先声明了一个Object类,假如判断是请求第二个用户的目录,那就用第二个用户的标准去强转对象,假如是系统管理员user的路径,那就直接转换成user对象。
而在转换完毕之后,我们需要把user存入本次信息中,这里使用了instanceof,即在强转前先判断这个user能不能强转过去,如果可以再进行塞值的行为。这部做完我们基本的修改成多Realm就基本完成了。
当然要注意这样是能适应多表了,可是原来的User相关的类也要修改的适应新环境才行,比如原来的ShiroDbRealm中的登陆验证必须要改成
然后在cn.stylefeng.guns.modular.system.controller下的LoginController类也要进行对应修改,没想到吧?
/**
* 点击登录执行的动作
*
* @author fengshuonan
* @Date 2018/12/23 5:42 PM
*/
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String loginVali() {
String username = super.getPara("username").trim();
String password = super.getPara("password").trim();
String remember = super.getPara("remember");
Subject currentUser = ShiroKit.getSubject();
UsernamePasswordToken token = new CustomLoginToken(username, password,"ShiroDb");
//如果开启了记住我功能
if ("on".equals(remember)) {
token.setRememberMe(true);
} else {
token.setRememberMe(false);
}
//执行shiro登录操作
currentUser.login(token);
//登录成功,记录登录日志
ShiroUser shiroUser = ShiroKit.getUserNotNull();
LogManager.me().executeLog(LogTaskFactory.loginLog(shiroUser.getId(), getIp()));
ShiroKit.getSession().setAttribute("sessionFlag", true);
return REDIRECT + "/";
}
主要是声明token的这个地方,要记得把原来的new UsernamePasswordToken改成new CustomLoginToken,并且在后面添加User表对应的Realm,因为只需要不会和其他Realm出现歧义就可以,所以我这里就写了ShiroDb而没有写全。
四、多增加一个用户需要写什么
现在我们通过走一趟基本了解整个Shrio在guns中的流程,那我们要加一个新的用户表需要做什么呢?
这里我们以Dl这个用户表为例,我们需要先实现对应表的架构,即Controller、Entity、Mapper、Service、Model,这些guns的代码生成器就可以帮我们实现,具体原理可以看上一篇。
把代码放到对应位置之后,我们还需要先做两件事,首先是建立一个用于登录的Controller,当然直接写在原来的上面也行,不过最好还是自己建一个。第二步就是再次到ShiroConfig中,为我们的登陆网页以及登陆接口暴露一下路径:
首先是登陆Controller的全部子目录暴露,其次是登陆使用的文件夹的子目录也暴露。在做完这些之后这两个路径才不会被拦截,不过似乎我还是有哪里没写好,所以不得不在AttributeSetInteceptor里面添加一段代码用来抵消拦截。代码如下:
通过这个判断,把所有访问agentLogin的请求都允许通过。
做完这些后基本的访问登陆网页又或者是登陆请求才不会出现404的问题。然后就是Realm的书写,其实没有什么大的问题,照着User的抄就好了,注意要写CustomLoginToken而不是UsernamePasswordToken其他没什么问题。
最后在ShiroConfig的list里面加一个项,然后对AttributeSetInteceptor进行补充,你的新用户就加入到Shrio的安全认证中了。
五、关于SpringBoot中文件的上传
Springboot中文件上传很方便,这里以Dl用户的注册为例。
/**
* 点击注册执行的动作
*
* @author fengshuonan
* @Date 2018/12/23 5:42 PM
*/
@RequestMapping(value = "/zhuce/", method = RequestMethod.POST)
public String tozhuce(@RequestParam("file") MultipartFile file,AgentManagementParam agentManagement) {
String fileName = agentManagement.getTel()+".jpg";
String filePath = "E:\\javahome\\guns\\src\\main\\webapp\\pages\\modular\\Tu\\";
File dest = new File(filePath + fileName);
try {
file.transferTo(dest);
} catch (IOException e) {
System.out.println("上传失败");
return PREFIX + "/agentRegister.html";
}
agentManagement.setQrCode(dest.getPath());
agentManagement.setPassword(ShiroKit.md5(agentManagement.getPassword(), "7753"));
System.out.println("注册信息:"+agentManagement);
this.agentManagementService.add(agentManagement);
System.out.println("注册成功");
return PREFIX + "/agentLogin.html";
}
这里的话同时接受了上传的文件以及注册的其他相关信息,通过.transferTo()这个方法对file文件进行了上传。这里有一个值得注意的地方,也是个困扰我很久的坑。
注意!!
在进行用户注册时候一定要记得对密码进行加盐加密,为什么呢?你要是看过guns的数据库,你就会发现user表的password都是乱码,也就是说guns是把加密之后的密码保存在数据库里面的,而他内部比对的也是两个密码加密之后是不是一样的。所以你在注册的时候就应该把密码先加密,这样到时候登陆时候才不会拿着加密过的密码和你原来的密码比对,死活登陆失败。
这里的ShiroKit.md5(agentManagement.getPassword(), "7753")是自带工具类中的加密方式,推荐直接使用而不是想什么骚操作,而我这里因为不需要加密密码,所以数据表并没有专门用来存盐的条目,这里直接用7753来作为了通用盐。