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

2017-12-25 12:00:31 +08:00
 javahih

目前在企业级项目里做权限安全方面喜欢使用 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

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

代码不忍直视

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/417384

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX