这篇文章讲的内容是在之前spring + mybatis + spring-mvc + freemarker框架整合的代码的基础上。有需要的可以看看我博客的前两篇文章。
另外,本文章所讲相关所有代码都已上传至github上:https://github.com/SonnAdolf/sonne_game
shiro是一个很有名的安全框架,功能也很多:登录的身份认证、权限管理、session会话管理、加密、缓存等等……
至于我目前开发的网站,需要用到的功能就三点:登录、权限、session。接下来我也只围绕这三点讲。
先谈我的需求,我的网站会有三种类型的人在使用,一是游客(未登录),二是普通用户、三是管理员。
我的设定是,网站主页任何用户都可以访问,个人空间只能登录用户访问,管理员页面只能管理员用户访问。
在user类里有这样一个字段:private boolean is_admin;用于区别于普通用户和管理员。
1,加入jar包、shiro-core-1.2.3.jar、shiro-spring-1.2.3.jar、shiro-web-1.2.3.jar
jackson-annotations-2.1.4.jar、jackson-core-2.1.4.jar、jackson-databind-2.1.4.jar(json相关,由于加入登录功能会遇到前端ajax请求后端返回json的情况)
log4j-1.2.16.jar
还有些jar包是我前两期博文写过的,这次就不提了。
2,web.xml里设置shiro的拦截器:
<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>*.form</url-pattern> </filter-mapping> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>*.ftl</url-pattern> </filter-mapping>
有了这个拦截器以后,前端后端请求都可以被shiro获取。
3,新建文件,spring-shiro.xml。
这是spring类型的配置文件,在web.xml里,下面这句配置
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:/spring-*.xml</param-value>
</context-param>
把所有spring-*.xml文件都包括了。
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.2.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.2.xsd" default-lazy-init="true"> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="" /> <property name="successUrl" value="" /> <property name="unauthorizedUrl" value="" /> <property name="filterChainDefinitions"> <value> /admin/** = roles[admin] /space/** = roles[user] </value> </property> </bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="authenticationRealm" /> </bean> <bean id="authenticationRealm" class="sonn.web.my_shiro.MyRealm"> </bean> </beans>
shiroFilter里面设置的loginUrl一类的我暂时都没填。filterChainDefinitions在我看来是重点,可以实现配置的指定url的权限管理。/admin/**=roles[admin]是指/admin/**路径需要admin权限。
securityManager是shiro核心概念之一。安全管理器。支配着所有subject(用户)的和安全相关的操作。可以视作为service层。subject是entity层。
authenticationRealm是另一个核心概念,realm。类似于dao层。这里的realm是自定义的,路径为sonn.web.my_shiro.MyRealm
4,自定义Realm
package sonn.web.my_shiro; import javax.annotation.Resource; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.IncorrectCredentialsException; 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.session.Session; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import sonn.web.entity.User; import sonn.web.mapper.UserMapper; import sonn.web.utils.Principal; /** * @ClassName: AuthenticationRealm * @Description: Shiro's realm * @author sonne * @date 2017-1-15 13:08:59 * @version 1.0 */ public class MyRealm extends AuthorizingRealm { @Resource(name = "userMapper") private UserMapper userMapper; @Override public String getName() { return "AuthenticationRealm"; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0) { SimpleAuthorizationInfo simpleAuthorInfo = new SimpleAuthorizationInfo(); Session session = this.getSession(); if (session == null) { return null; } Principal principal = (Principal) session.getAttribute("currentUser"); String role = principal.getRole(); if (role.equals("user")) { simpleAuthorInfo.addRole("user"); } if (role.equals("admin")) { simpleAuthorInfo.addRole("admin"); } return simpleAuthorInfo; } @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); String password = new String((char[]) token.getCredentials()); User db_usr = userMapper.findByUsername(username); if (!db_usr.getUsrname().equals(username)) { throw new UnknownAccountException(); } if (!db_usr.getPasswd().equals(password)) { throw new IncorrectCredentialsException(); } SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo( username, password, getName()); String role; if (db_usr.isIs_admin()) { role = "admin"; } else { role = "user"; } Principal principal = new Principal(db_usr.getId(), username, role); this.setSession("currentUser", principal); return simpleAuthenticationInfo; } private void setSession(Object key, Object value) { Subject currentUser = SecurityUtils.getSubject(); if (null != currentUser) { Session session = currentUser.getSession(); System.out .println("Session默认超时时间为[" + session.getTimeout() + "]毫秒"); if (null != session) { session.setAttribute(key, value); } } } private Session getSession() { Subject currentUser = SecurityUtils.getSubject(); if (null != currentUser) { Session session = currentUser.getSession(); return session; } return null; } }
这里面setSession和getSession就是shiro的session的get、set方法,不多说。
核心是AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection arg0)方法和AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException
。也就是继承AuthorizingRealm方法必须要实现的方法
前者是每次用户访问权限限制url(见第三步filterChainDefinitions里的设置)都会访问这个方法,来检查是否具有权限。代码中通过获取session,然后检查session中设置的权限。(用户登录成功后设置session这是前提)
后者是登录过程通过用户输入的用户名和密码进行权限确认的方法。用户名和密码通过AuthenticationToken token参数获取。之后查询数据库检验用户信息,若登录成功则设置session信息。
Principal principal = new Principal(db_usr.getId(), username, role); this.setSession("currentUser", principal);
session中包含用户id、用户名、和角色。
至于mybatis的数据库操作,通过spring标签将mapper的bean注入进来了,之后直接使用即可:
@Resource(name = "userMapper") private UserMapper userMapper;
有必要注明一点:由于目前还没完全地做成一个登录功能,所以登录过程没有加密处理,也没有验证码。最主要目的是整合shiro框架。
5,登录controller
package sonn.web.controller; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.ExcessiveAttemptsException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.subject.Subject; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.ResponseBody; import sonn.web.entity.User; import com.alibaba.fastjson.JSONObject; /** * @ClassName: LoginController * @Description: Controller of login * @author sonne * @date 2017-1-15 13:07:00 * @version 1.0 */ @Controller @RequestMapping("/login") public class LoginController { /* * show the web page of login action. */ @RequestMapping(value = "/show", method = RequestMethod.GET) public String submit(HttpServletRequest request, Model model) throws Exception { String path = request.getContextPath(); String basePath = request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + path; model.addAttribute("base", basePath); return "login"; } /* * login submit, check, save the session. */ @RequestMapping(value = "/login", method = RequestMethod.POST) @ResponseBody public JSONObject submit(HttpServletRequest request, HttpServletResponse response, User usr) throws Exception { JSONObject jo = new JSONObject(); String usrname = usr.getUsrname(); String passwd = usr.getPasswd(); UsernamePasswordToken token = new UsernamePasswordToken(usrname, passwd); Subject subject = SecurityUtils.getSubject(); try { subject.login(token); } catch (IncorrectCredentialsException ice) { jo.put("success", false); jo.put("msg", "密码错误"); return jo; } catch (UnknownAccountException uae) { jo.put("success", false); jo.put("msg", "未知用户名"); return jo; } catch (ExcessiveAttemptsException eae) { jo.put("success", false); jo.put("msg", "登录次数过多"); return jo; } jo.put("success", true); jo.put("msg", "登录成功"); return jo; } }
这里通过设置UsernamePasswordToken这个token给Realm来进行校验:
String usrname = usr.getUsrname(); String passwd = usr.getPasswd(); UsernamePasswordToken token = new UsernamePasswordToken(usrname, passwd); Subject subject = SecurityUtils.getSubject(); try { subject.login(token); } catch (IncorrectCredentialsException ice) { …… }
Subject subject = SecurityUtils.getSubject();这样的写法应该是工厂模式吧。
6,登录功能相关、新增根据用户名查询用户的dao层操作
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace = "sonn.web.mapper.UserMapper"> <select id = "findAll" resultType = "sonn.web.entity.User"> select ID,USRNAME,PASSWD,IS_ADMIN from USER </select> <select id = "findByUsername" parameterType="String" resultType = "sonn.web.entity.User"> select ID,USRNAME,PASSWD,IS_ADMIN from USER where USRNAME = #{usrname} </select> </mapper>
需要注意mybatis里select ID,USRNAME,PASSWD,IS_ADMIN from USER where USRNAME = #{usrname}这样的写法。
7,登录功能前端方面、一些与shiro不太相关的介绍:
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>sonne_game</title> </head> <body> <p>Here,log in</p> <form id="loginForm" action="/Sonne_game/login/login.form" method="post"> usrname:<input name="usrname"/><br> passwd:<input name="passwd"/><br> <button type="submit" id="submit" style="height:20px;55px;">submit</button> </form> <script type="text/javascript" src="${base}/Jquery/jquery-1.3.1.js"></script> <script type="text/javascript" src="${base}/Jquery/jquery.form.js"></script> <script type="text/javascript"> $(document).ready(function() { $('#loginForm').ajaxForm({ dataType: 'json', beforeSubmit: validate, success: successFunc }); }); function validate(formData, jqForm, options) { return true; } function successFunc(data) { if (data.success) { alert("登录成功:"+" " + data.msg); } else { alert("登录失败:"+" " + data.msg); } } </script> </body> </html>
登录这类操作操作需要使用ajax操作。这方面原始的javascript是比较麻烦的。一般用jquery的组件。jquery.form.js。具体先不讲了。
本系列下一篇大概会认真搞一搞前端,做一个全力发挥自己前端水平的登录页面(虽然不是前端程序员)。
主要会研究下响应式页面和滑动式验证码~
8,目前达到的效果:
项目结构:
登录:
只有admin类型的user能访问admin管理页面,同理只有普通用户能访问个人空间页面: