V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
javahih
V2EX  ›  Java

基于权限安全框架 Shiro 的登录验证功能实现

  •  
  •   javahih · 2017-12-25 12:00:31 +08:00 · 3755 次点击
    这是一个创建于 2560 天前的主题,其中的信息可能已经有所发展或是发生改变。

    目前在企业级项目里做权限安全方面喜欢使用 Apache 开源的 Shiro 框架或者 Spring 框架的子框架 Spring Security。

    Apache Shiro 是一个强大且易用的 Java 安全框架,执行身份验证、授权、密码学和会话管理。

    Shiro 框架具有轻便,开源的优点,所以本博客介绍基于 Shiro 的登录验证实现。

    本博客只提供基于 Shiro 的登录验证实现,具体代码可以去我的 github 下载: https://github.com/u014427391/jeeplatform 欢迎 star

    在 maven 里加入 shiro 需要的 jar

    <!--shiro start-->
          <dependency>
                <groupId>org.apache.shiro</groupId>
                <artifactId>shiro-all</artifactId>
                <version>1.2.3</version>
           </dependency>
    <!-- shiro end-->
    

    在 web.xml 加上 Shiro 过滤器配置:

    
      <!-- Shiro 过滤器配置 start -->
       <filter>
         <filter-name>shiroFilter</filter-name>
         <filter-class>
                 org.springframework.web.filter.DelegatingFilterProxy
             </filter-class>
         <init-param>
           <param-name>targetFilterLifecycle</param-name>
           <param-value>true</param-value>
         </init-param>
       </filter>
       <filter-mapping>
         <filter-name>shiroFilter</filter-name>
         <url-pattern>/*</url-pattern>
       </filter-mapping>
      <!-- Shiro 过滤器配置 end -->
    
    

    编写 shiro 的 ShiroRealm 类:

    package org.muses.jeeplatform.core.security.shiro;
    
    import javax.annotation.Resource;
    
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.AuthenticationInfo;
    import org.apache.shiro.authc.AuthenticationToken;
    import org.apache.shiro.authc.LockedAccountException;
    import org.apache.shiro.authc.SimpleAuthenticationInfo;
    import org.apache.shiro.authc.UnknownAccountException;
    import org.apache.shiro.authz.AuthorizationInfo;
    import org.apache.shiro.authz.SimpleAuthorizationInfo;
    import org.apache.shiro.realm.AuthorizingRealm;
    import org.apache.shiro.subject.PrincipalCollection;
    import org.muses.jeeplatform.model.entity.User;
    import org.muses.jeeplatform.service.UserService;
    
    /**
     * @description 基于 Shiro 框架的权限安全认证和授权
     * @author Nicky
     * @date 2017 年 3 月 12 日
     */
    public class ShiroRealm extends AuthorizingRealm {
    
    	/**注解引入业务类**/
    	@Resource
    	UserService userService;
    	
    	/**
    	 * 登录信息和用户验证信息验证(non-Javadoc)
    	 * @see org.apache.shiro.realm.AuthenticatingRealm#doGetAuthenticationInfo(AuthenticationToken)
    	 */
    	@Override
    	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
    
    		 String username = (String)token.getPrincipal();  				//得到用户名 
    	     String password = new String((char[])token.getCredentials()); 	//得到密码
    	     
    	     User user = userService.findByUsername(username);
    
    	     /**检测是否有此用户 **/
    	     if(user == null){
    	    	 throw new UnknownAccountException();//没有找到账号异常
    	     }
    	     /**检验账号是否被锁定 **/
    	     if(Boolean.TRUE.equals(user.getLocked())){
    	    	 throw new LockedAccountException();//抛出账号锁定异常
    	     }
    	     /**AuthenticatingRealm 使用 CredentialsMatcher 进行密码匹配**/
    	     if(null != username && null != password){
    	    	 return new SimpleAuthenticationInfo(username, password, getName());
    	     }else{
    	    	 return null;
    	     }
    	     
    	}
    	
    	/**
    	 * 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用,负责在应用程序中决定用户的访问控制的方法(non-Javadoc)
    	 * @see AuthorizingRealm#doGetAuthorizationInfo(PrincipalCollection)
    	 */
    	@Override
    	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection pc) {
    		String username = (String)pc.getPrimaryPrincipal();
    		SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
    	    authorizationInfo.setRoles(userService.getRoles(username));
    	    authorizationInfo.setStringPermissions(userService.getPermissions(username));
    		System.out.println("Shiro 授权");
    	    return authorizationInfo;
    	}
    	
    	 @Override
    	 public void clearCachedAuthorizationInfo(PrincipalCollection principals) {
    		 super.clearCachedAuthorizationInfo(principals);
    	 }
    
    	 @Override
    	 public void clearCachedAuthenticationInfo(PrincipalCollection principals) {
    	     super.clearCachedAuthenticationInfo(principals);
    	 }
    
    	 @Override
    	 public void clearCache(PrincipalCollection principals) {
    	      super.clearCache(principals);
    	 }
    
    }
    
    

    在 Spring 框架里集成 Shiro,加入配置

    <!--  Shiro start  -->
    		<bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
    			<property name="realm" ref="ShiroRealm" />
    		</bean>
    		
    		<!-- 项目自定义的 Realm -->
    	    <bean id="ShiroRealm" class="org.muses.jeeplatform.core.security.shiro.ShiroRealm" ></bean>
    		
    		<!-- Shiro Filter -->
    		<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    			<property name="securityManager" ref="securityManager" />
    			
    			<property name="loginUrl" value="/login" />
    			
    			<property name="successUrl" value="/admin/index" />
    			
    			<property name="unauthorizedUrl" value="/login" />
    			
    			<property name="filterChainDefinitions">
    				<value>
    				/static/**					= anon
    				/upload/**			    	= anon
    				/plugins/** 				= anon
    	           	/code 						= anon
    	           	/login    	 	       		= anon
    	           	/logincheck					= anon
    	           	/**							= authc
    				</value>
    			</property>
    		</bean>
    	<!--  Shiro end  -->	
    

    登录验证控制类实现:

    package org.muses.jeeplatform.web.controller;
    
    import java.util.ArrayList;
    import java.util.HashMap;
    import java.util.HashSet;
    import java.util.List;
    import java.util.Map;
    import java.util.Set;
    
    import javax.servlet.http.HttpServletRequest;
    
    import net.sf.json.JSONArray;
    import net.sf.json.JSONObject;
    
    import org.apache.shiro.SecurityUtils;
    import org.apache.shiro.authc.AuthenticationException;
    import org.apache.shiro.authc.UsernamePasswordToken;
    import org.apache.shiro.crypto.hash.SimpleHash;
    import org.apache.shiro.session.Session;
    import org.apache.shiro.subject.Subject;
    import org.muses.jeeplatform.core.Constants;
    import org.muses.jeeplatform.model.entity.Menu;
    import org.muses.jeeplatform.model.entity.Permission;
    import org.muses.jeeplatform.model.entity.Role;
    import org.muses.jeeplatform.model.entity.User;
    import org.muses.jeeplatform.service.MenuService;
    import org.muses.jeeplatform.service.UserService;
    import org.muses.jeeplatform.utils.Tools;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.servlet.ModelAndView;
    
    /**
     * @description 登录操作的控制类,使用 Shiro 框架,做好了登录的权限安全认证,
     * getRemortIP()方法获取用户登录时的 ip 并保存到数据库
     * @author Nicky
     * @date 2017 年 3 月 15 日
     */
    @Controller
    public class LoginController extends BaseController {
    	
    	@Autowired
    	UserService userService;
    	@Autowired
    	MenuService menuService;
    	
    	/**
    	 * 获取登录用户的 IP
    	 * @throws Exception 
    	 */
    	public void getRemortIP(String username)  {  
    		HttpServletRequest request = this.getRequest();
    		Map<String,String> map = new HashMap<String,String>();
    		String ip = "";
    		if (request.getHeader("x-forwarded-for") == null) {  
    			ip = request.getRemoteAddr();  
    	    }else{
    	    	ip = request.getHeader("x-forwarded-for");  
    	    }
    		map.put("username", username);
    		map.put("loginIp", ip);
    		 userService.saveIP(map);
    	}  
    	
    	/**
    	 * 访问后台登录页面
    	 * @return
    	 * @throws Exception
    	 */
    	@RequestMapping(value="/login",produces="text/html;charset=UTF-8")
    	public ModelAndView toLogin()throws ClassNotFoundException{
    		ModelAndView mv = this.getModelAndView();
    		mv.setViewName("admin/frame/login");
    		return mv;
    	}
    	
    	/**
    	 * 基于 Shiro 框架的登录验证,页面发送 JSON 请求数据,
    	 * 服务端进行登录验证之后,返回 Json 响应数据,"success"表示验证成功
    	 * @param request
    	 * @return
    	 * @throws Exception
    	 */
    	@RequestMapping(value="/logincheck", produces="application/json;charset=UTF-8")
    	@ResponseBody
    	public String loginCheck(HttpServletRequest request)throws AuthenticationException{
    		JSONObject obj = new JSONObject();
    		String errInfo = "";//错误信息
    		String logindata[] = request.getParameter("LOGINDATA").split(",");
    		if(logindata != null && logindata.length == 3){
    			//获取 Shiro 管理的 Session
    			Subject subject = SecurityUtils.getSubject();
    			Session session = subject.getSession();
    			String codeSession = (String)session.getAttribute(Constants.SESSION_SECURITY_CODE);
    			String code = logindata[2]; 
    			/**检测页面验证码是否为空,调用工具类检测**/
    			if(Tools.isEmpty(code)){
    				errInfo = "nullcode";
    			}else{
    				String username = logindata[0];
    				String password = logindata[1];
    				if(Tools.isNotEmpty(codeSession) && codeSession.equalsIgnoreCase(code)){
    					//Shiro 框架 SHA 加密
    					String passwordsha = new SimpleHash("SHA-1",username,password).toString();
    					System.out.println(passwordsha);
    					//检测用户名和密码是否正确
    					User user = userService.doLoginCheck(username,passwordsha);
    					if(user != null){
    						if(Boolean.TRUE.equals(user.getLocked())){
    							errInfo = "locked";
    						}else{
    							//Shiro 添加会话
    							session.setAttribute("username", username);
    							session.setAttribute(Constants.SESSION_USER, user);
    							//删除验证码 Session
    							session.removeAttribute(Constants.SESSION_SECURITY_CODE);
    							//保存登录 IP
    							getRemortIP(username);
    							/**Shiro 加入身份验证**/
    							Subject sub = SecurityUtils.getSubject();
    							UsernamePasswordToken token = new UsernamePasswordToken(username,password);
    							sub.login(token);
    						}
    					}else{
    						//账号或者密码错误
    						errInfo = "uerror";
    					}
    					if(Tools.isEmpty(errInfo)){
    						errInfo = "success";
    					}
    				}else{
    					//缺少参数
    					errInfo="codeerror";
    				}
    			}
    		}
    		obj.put("result", errInfo);
    		return obj.toString();
    	}
    		
    	/**
    	 * 后台管理系统主页
    	 * @return
    	 * @throws Exception
    	 */
    	@RequestMapping(value="/admin/index")
    	public ModelAndView toMain() throws AuthenticationException{
    		ModelAndView mv = this.getModelAndView();
    		/**获取 Shiro 管理的 Session**/
    		Subject subject = SecurityUtils.getSubject();
    		Session session = subject.getSession();
    		User user = (User)session.getAttribute(Constants.SESSION_USER);
    		
    		if(user != null){
    			...//业务实现
    		}else{
    			//会话失效,返回登录界面
    			mv.setViewName("admin/frame/login");
    		}
    		mv.setViewName("admin/frame/index");
    		return mv;
    	}
    	
    	/**
    	 * 注销登录
    	 * @return
    	 */
    	@RequestMapping(value="/logout")
    	public ModelAndView logout(){
    		ModelAndView mv = this.getModelAndView();
    		/**Shiro 管理 Session**/
    		Subject sub = SecurityUtils.getSubject();
    		Session session = sub.getSession();
    		session.removeAttribute(Constants.SESSION_USER);
    		session.removeAttribute(Constants.SESSION_SECURITY_CODE);
    		/**Shiro 销毁登录**/
    		Subject subject = SecurityUtils.getSubject();
    		subject.logout();
    		/**返回后台系统登录界面**/
    		mv.setViewName("admin/frame/login");
    		return mv;
    	}
    
    
    }
    
    

    前端 Ajax 和 JQeury 校验实现:

     /**客户端校验**/
        function checkValidity() {
    
            if ($("#username").val() == "") {
    
                $("#username").tips({
                    side : 2,
                    msg : '用户名不得为空',
                    bg : '#AE81FF',
                    time : 3
                });
    
                $("#username").focus();
                return false;
            }
    
            if ($("#password").val() == "") {
                $("#password").tips({
                    side : 2,
                    msg : '密码不得为空',
                    bg : '#AE81FF',
                    time : 3
                });
    
                $("#password").focus();
                return false;
            }
            if ($("#code").val() == "") {
    
                $("#code").tips({
                    side : 1,
                    msg : '验证码不得为空',
                    bg : '#AE81FF',
                    time : 3
                });
    
                $("#code").focus();
                return false;
            }
    
            return true;
        }
    
        /**服务器校验**/
        function loginCheck(){
            if(checkValidity()){
                var username = $("#username").val();
                var password = $("#password").val();
                var code = username+","+password+","+$("#code").val();
                $.ajax({
                    type: "POST",//请求方式为 POST
                    url: 'logincheck',//检验 url
                    data: {LOGINDATA:code,tm:new Date().getTime()},//请求数据
                    dataType:'json',//数据类型为 JSON 类型
                    cache: false,//关闭缓存
                    success: function(data){//响应成功
                        if("success" == data.result){
                            $("#login").tips({
                                side : 1,
                                msg : '正在登录 , 请稍后 ...',
                                bg : '#68B500',
                                time : 10
                            });
                            window.location.href="admin/index";
                        }else if("uerror" == data.result){
                            $("#username").tips({
                                side : 1,
                                msg : "用户名或密码有误",
                                bg : '#FF5080',
                                time : 15
                            });
                            $("#username").focus();
                        }else if("codeerror" == data.result){
                            $("#code").tips({
                                side : 1,
                                msg : "验证码输入有误",
                                bg : '#FF5080',
                                time : 15
                            });
                            $("#code").focus();
                        }else if("locked" == data.result){
                            alert('您的账号被锁定了,呜呜');
                        }else{
                            $("#username").tips({
                                side : 1,
                                msg : "缺少参数",
                                bg : '#FF5080',
                                time : 15
                            });
                            $("#username").focus();
                        }
                    }
                });
            }
        }
    

    这里写图片描述

    登录成功,Session 会话过期,需要重新登录,保证系统安全性 这里写图片描述

    本博客只提供基于 Shiro 的登录验证实现,具体代码可以去我的 github 下载: https://github.com/u014427391/jeeplatform 欢迎 star

    6 条回复    2017-12-25 15:29:19 +08:00
    ymcisokay
        1
    ymcisokay  
       2017-12-25 13:18:36 +08:00
    最近刚在学 shiro,mark 一下。
    biaoliruyi
        2
    biaoliruyi  
       2017-12-25 14:37:03 +08:00
    mark
    lhx2008
        4
    lhx2008  
       2017-12-25 14:45:49 +08:00 via Android
    用自定义注解和拦截器撸了一个,勉强够用
    lhx2008
        5
    lhx2008  
       2017-12-25 14:51:05 +08:00 via Android
    在 controller 加类注解或 action 者方法注解,拦截器那边把注解读出来,就可以做不同用户组的鉴权了,还可以顺带加上 user 自动注入
    qinxi
        6
    qinxi  
       2017-12-25 15:29:19 +08:00
    我就想说

    代码不忍直视
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2470 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 15:42 · PVG 23:42 · LAX 07:42 · JFK 10:42
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.