记录我在学习guns框架时候的二三事(二)

一、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来作为了通用盐。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值