ssm-shiro的简单使用
写在前面:小仙呕心沥血学习之笔记,不足之处请指出,转载请注明出处.
环境:idea,mysql,jdk8
1 为什么要使用shiro?
项目中的密码在数据库中的存储要保障安全性,不被恶意攻击盗取,就不能使用明文存储,最好使用不可逆转的加密方式处理之后再存进数据库中;
项目网站的页面,请求是否可以任意访问,是否需要登录后访问一些功能,是不是需要做一些相关的安全性校验?
项目中的各种操作,是不是任意的操作者都可以进行操作,有没有一个身份的区分?
综上所述,我们需要进行一些安全校验以保障整个系统的安全,维护一个良好的运行秩序.
2 Shiro简介:
精简了Shiro的主要概要,不啰嗦,如下所示:
- 三个关键字:Simple,Java,Security,即是Java的一个轻量级的安全框架:
- 四大主要功能:Authentication,Authorization,Cryptography,Session Manager,即身份认证,权限验证,加密,会话管理,基本结构如下图:
- 三大护法(运行流程中的三个核心组件):Subject,SecurityManager,Realm,即当前用户,安全管理器(管理着所有的Subject),安全访问数据的入口(安全管理器SecurityManager要从Realm中获取用户信息进行比对校验),流程如下图:
- RBAC模型:Role Base Access Control基于角色的访问控制,该模型中主要有3个主体:用户,角色和权限,如下图:
5.底层源码实现,上图:
3 与ssm项目的集成
- SSM框架是我自己封装的一个archetype,关于SSM框架的搭建后续有时间会写一篇博文进行补充,在这里我们基于这个框架做shiro的集成.
- 做完了才发现我只是做了一个与web的集成,并没有做与Spring的集成,不过没影响,与web集成的不同的地方就是通过spring集成有,可以直接将shiro.ini文件中的配置的组件,迁移到spring的配置文件中,依赖换成shiro-spring就好啦,有兴趣可以自己试一下.
3.1 pom.xml
首先,在项目的pom.xml文件中加入相关的依赖
<!-- shiro --> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> <!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-web --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.4.0</version> </dependency>
3.2 web.xml
接下来第二步,在WEB-INF目录下的web.xml文件中安装ShiroFilter(就是做了一些配置,要知道,整个web项目中在项目刚开始运行时,真正加载的资源文件只有这个web.xml文件,resources下的文件也是通过在这个文件中做一些路径的配置加载容器啊之类的):
献上在web.xml文件中shiro的主要配置,相关讲解看代码前面的注释:
<!-- ShiroFilter:shiro集成的核心过滤器 --> <!-- 可以理解为是一个安保人员,加载web.xml文件后时刻在岗位上等待接收所有的请求并过滤: 1.其实就是通过请求路径,识别一下是否需要安全校验,需要就触发安全校验 2.记住这个过滤器的名字,后面的shiro.ini文件中需要使用shiroFilter配置加密--> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.apache.shiro.web.servlet.ShiroFilter</filter-class> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- 监听:项目启动后,随之启动shiro,加载WEB-INF或classpath(resources目录下)的shiro.ini文件(我的是放在classpath下),构建SecurityManager 1.会构建出所有配置中使用的过滤器链(anno,authc等),ShiroFilter会获取系过滤器链 --> <listener> <listener-class>org.apache.shiro.web.env.EnvironmentLoaderListener</listener-class> </listener> <!-- 若除了shiro之外你自己定义了其他的.ini文件,则需要通过下面的配置来指定一下自定义文件的名称和位置,如果只有shiro.ini一个文件,那么下面的这个配置不用要 --> <context-param> <param-name>shiroConfigLocation</param-name> <param-value>classpath:shiro_1.ini</param-value> </context-param>
到这里,web.xml文件中的shiro的配置就可以了.
3.3 shiro.ini
-
首先这个文件放在WEB-INF目录下还是放在resources目录下,全看个人喜好了,多个.ini文件,就要注意一下在web.xml中配置路径的问题的,我的是放在resources目录下的;
shiro.ini文件有四大配置:[users],[roles],[main],[urls],我们以代码的例子分别介绍标签的主要作用,这个纯属介绍,让你理解什么是什么,用来干什么的(我这样的良心博主不多了,看到这里,还不甩手就是一个大大的赞,再关注一波?),后面再附上准确的shiro.ini文件的相关配置;
# users:在上面的RBAC(Role Base Access Control)中介绍了shiro的3个主体:用户,角色和权限,这里就是用户和角色的配置: # 下面users的配置可以解释为:在这个文件中配置了两个用户: # 1.用户名为zhangsan,密码为111,角色既是teacher又是professor # 2.用户名为lisi,密码为222,角色为student [users] zhangsan = 111,teacher,professor lisi = 222,student, # roles:主要是对users中定义的角色相关的权限的配置 # 下面roles的配置可以理解为: # 1.teacher角色拥有所有的权限 # 2.professor角色拥有user的query和insert的权限 # 3.student角色拥有user下的所有操作权限 [roles] teacher = * professor = user:query,user:insert student = user:* [main] # 没有身份认证时的跳转路径(这里设置转去登录页) shiro.loginUrl = /user/login/page # 角色或者权限的校验不通过时的跳转路径 shiro.unauthorizedUrl = /login.jsp # 登出(注销)后的跳转路径 shiro.redirectUrl = /logout.jsp # 由于浏览器可能会有预加载,所以如果登出的请求是get方式,有可能导致用户意外退出,这里强制设置登出的请求方式为post shiro.postOnlyLogout = true # urls:主要是对访问的请求路径做的安全校验,理解为是一个过滤器拦截吧 # 如:/user/login/page = anon:anno为不需要登录就可以访问,即不需要身份认证 # authc配置的请求路径需要身份认证才能访问 # roles配置请求路径能访问的角色 # perms配置拥有user:update权限或者user:delete权限的才能访问 # logout登出的路径(不用在controller中写一个Handler) [urls] /user/login/page = anon /user/query = authc /user/update = authc,roles["manager","seller"] /user/delete = authc, perms["user:update","user:delete"] /user/logout = logout # 下面的这个要慎用啊(其余路径都需要身份认证),最好是能配置/user/* = authc:user下的其他路径都要身份验证 /** = authc
献上[urls]中其他默认的过滤器,如下图:
-
由于后面会用自定义的Realm,所以我们的shiro.ini文件配置简化成如下配置:
[urls] /user/query=anon /user/insert=authc,roles["banfu"] /user/update=authc,perms["student:update"] /user/delete=authc,roles["xuewei"] /user/logout=logout [main] shiro.loginUrl = /user/login/page shiro.unauthorizedUrl=/error.jsp shiro.redirectUrl=/index.jsp shiro.postOnlyLogout = true #声明密码比对器 credentialsMatcher=org.apache.shiro.authc.credential.HashedCredentialsMatcher credentialsMatcher.hashAlgorithmName=sha-256 credentialsMatcher.hashIterations=1024 #true=hex格式 false=base64格式 credentialsMatcher.storedCredentialsHexEncoded=false #比对器关联给realm,则realm中对用户做身份认证时,可以使用加密比对器,对密文做比对 shiroFilter = com.shiro.realm.MyRealm shiroFilter.credentialsMatcher=$credentialsMatcher securityManager.realms=$shiroFilter
3.4 jsp
-
在这里定义Controller中 和 shiro.ini中需要的jsp页面,简单的测试,我就简单的写了
在webapp目录下定义:index.jsp
<%@ taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <body> <h2>Index.jsp</h2> <%-- 是否登录 或 记住我(cookie) --%> <shiro:user> Hello,<shiro:principal/>| </shiro:user> <%-- 既没有登录 也 没有记住我 --%> <shiro:guest> Hello, please <a href="${pageContext.request.contextPath}/user/login/page">login</a> <a href="${pageContext.request.contextPath}/user/regist/page">Regist</a>| </shiro:guest> <hr /> <%--登录后才能判断--%> <shiro:authenticated> <shiro:lacksRole name="banzhang"> Hello,<shiro:principal/> student. </shiro:lacksRole> </shiro:authenticated> <hr /> <%--登录后显示登出--%> <shiro:authenticated> <form action="${pageContext.request.contextPath}/user/logout" method="post"> <input type="submit" value="Logout"> </form> </shiro:authenticated> </body> </html>
在webapp目录下定义:error.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Error</title> </head> <body> <h2>Permission denied</h2> </body> </html>
在webapp/WEB-INF目录下定义:login.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Login</title> </head> <body> <form action="${pageContext.request.contextPath}/user/login/logic" method="put" enctype="application/x-www-form-urlencoded"> UserName:<input type="text" id="username" name="username"><br/> PassWord:<input type="text" id="password" name="password"><br/> <input type="submit" value="Login"> </form> <%--后台没往session中放东西,你可以自己加上--%> <script type="text/javascript"> if ('${sessionScope.get("msg")}'==1){ <%session.setAttribute("msg", 0);%> alert("Username or password wrong!Please try again!"); } </script> </body> </html>
在webapp/WEB-INF目录下定义:regist.jsp
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Regist</title> </head> <body> <form action="${pageContext.request.contextPath}/user/regist/logic" method="put" enctype="application/x-www-form-urlencoded"> UserName:<input type="text" id="username" name="username"><br/> PassWord:<input type="text" id="password" name="password"><br/> <input type="submit" value="Regist"> </form> <%--后台没往session中放东西,你可以自己加上--%> <script type="text/javascript"> if ('${sessionScope.get("msg")}'==1){ <%session.setAttribute("msg", 0);%> alert("Regist failure!Please try again!"); } </script> </body> </html>
3.5 自定义Realm(有加密)
- 这一节我们从数据库开始说,从后向前dao,service,controller,自定义realm类,只能说,到位了!
3.5.1 mysql建表
-
一共5张表:用户表,角色表,用户角色关联表,权限表,权限角色关联表
这里只给出建表语句,表中数据的添加不再赘述:
create database usershiro default charset=utf8; create table t_user( id int primary key auto_increment, username varchar(20) not null unique, password varchar(100) not null )engine=innodb default charset=utf8; create table t_role( id int primary key auto_increment, role_name varchar(50) not null unique, create_time timestamp not null )engine=innodb default charset=utf8; create table t_permission( id int primary key auto_increment, permission_name varchar(50) not null unique, create_time timestamp )engine=innodb default charset=utf8; create table t_user_role( id int primary key auto_increment, user_id int references t_user(id), role_id int references t_role(id), unique(user_id,role_id) )engine=innodb default charset=utf8; create table t_role_permission( id int primary key auto_increment, permission_id int references t_user(id), role_id int references t_role(id), unique(permission_id,role_id) )engine=innodb default charset=utf8;
3.5.2 bean
- 我们只用到了User.java,根据数据库表中的属性,创建一个用户类是闭着眼睛也能做出来的事,再贴这个代码就说明low爆了…略过~~
3.5.3 dao
-
com.shiro.dao目录下主要是放了dao层的封装接口和.xml映射实现文件
-
我们在这里只实现用户登录注册的shiro安全校验
UserMapper.java
package com.shiro.dao; import com.shiro.bean.User; public interface UserMapper { public User queryUserByUsername(String username); public Integer insertUser(User user); }
UserMapper.xml
<?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="com.shiro.dao.UserMapper"> <select id="queryUserByUsername" resultType="User" parameterType="string"> select id,username,password,salt from t_user where username = #{username} </select> <insert id="insertUser" parameterType="User"> insert into t_user(username,password,salt) values(#{username},#{password},#{salt}) </insert> </mapper>
RoleMApper.java
package com.shiro.dao; import java.util.Set; public interface RoleMapper { public Set<String> queryRoleByUsername(String username); }
RoleMapper.xml
<?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="com.shiro.dao.RoleMapper"> <select id="queryRoleByUsername" resultType="string" parameterType="string"> select t_role.role_name from t_user join t_user_role on t_user.id = t_user_role.user_id join t_role ON t_role.id = t_user_role.role_id where t_user.username = #{username} </select> </mapper>
PermissionMapper.java
package com.shiro.dao; import java.util.Set; public interface PermissionMapper { public Set<String> queryPermissionByUsername(String username); }
PermissionMapper.xml
<?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="com.shiro.dao.PermissionMapper"> <select id="queryPermissionByUsername" resultType="string" parameterType="string"> select t_permission.permission_name from t_user join t_user_role on t_user.id = t_user_role.user_id join t_role ON t_role.id = t_user_role.role_id join t_role_permission on t_role.id = t_role_permission.role_id JOIN t_permission on t_role_permission.permission_id=t_permission.id where t_user.username = #{username} </select> </mapper>
3.5.4 service
-
com.shiro.service需要注意的一点就是用户在注册时,密码在这层做了加密的操作,这个加密就很恶心,让人看了就不想去破解的那种;
-
加密原理:属于加盐多次迭代的加密过程,举个例子吧,我们用sha-256进行加密,加入迭代了两次,第一次 sha-256(明文+salt)=密文a 第二次迭代 sha-256(密文a+salt)=最终密文
-
建议迭代次数为1000+,没开玩笑,严肃点不许笑!哈哈哈
UserService.java
package com.shiro.service; import com.shiro.bean.User; public interface UserService { public User queryUserByUsername(String username); public Integer createUser(User user); }
UserServiceImpl.java
package com.shiro.service; import com.shiro.bean.User; import com.shiro.dao.UserMapper; import org.apache.shiro.crypto.hash.Sha256Hash; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.UUID; @Service public class UserServiceImpl implements UserService { @Autowired UserMapper userMapper; @Override public User queryUserByUsername(String username) { return userMapper.queryUserByUsername(username); } @Transactional public Integer createUser(User user) { String salt = UUID.randomUUID().toString(); // 将用户密码加密 String password = new Sha256Hash(user.getPassword(), salt, 1024).toBase64(); // 覆盖用户输入的明文密码 user.setPassword(password); user.setSalt(salt); Integer count = userMapper.insertUser(user); return count; } }
RoleService.java
package com.shiro.service; import java.util.Set; public interface RoleService { public Set<String> queryRoleByUsername(String username); }
RoleServiceImpl.java
package com.shiro.service; import com.shiro.dao.RoleMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Set; @Service public class RoleServiceImpl implements RoleService { @Autowired RoleMapper roleMapper; @Override public Set<String> queryRoleByUsername(String username) { return roleMapper.queryRoleByUsername(username); } }
PermissionService.java
package com.shiro.service; import java.util.Set; public interface PermissionService { public Set<String> queryPermissionByUsername(String username); }
PermissionServiceImpl.java
package com.shiro.service; import com.shiro.dao.PermissionMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Set; @Service public class PermissionServiceImpl implements PermissionService { @Autowired PermissionMapper permissionMapper; @Override public Set<String> queryPermissionByUsername(String username) { return permissionMapper.queryPermissionByUsername(username); } }
3.5.5 controller
-
这里仅用了一个UserController,做了登录注册的实现,其他的模拟
UserController.java
package com.shiro.controller; import com.shiro.bean.User; import com.shiro.service.UserService; import org.apache.shiro.SecurityUtils; 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.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; @Controller @RequestMapping("/user") public class UserController{ @Autowired UserService userService; @RequestMapping("/query") public String userQuery(){ System.out.println("User query..."); return "index"; } @RequestMapping("/insert") public String userInsert(){ System.out.println("User insert..."); return "index"; } @RequestMapping("/update") public String userUpdate(){ System.out.println("User update..."); return "index"; } @RequestMapping("/delete") public String userDelete(){ System.out.println("User delete..."); return "index"; } @RequestMapping("/login/page") public String userLoginPage(){ System.out.println("User login page..."); return "WEB-INF/login"; } @RequestMapping("/login/logic") public String userLoginLogic(String username,String password){ System.out.println("User login logic..."); UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject subject = SecurityUtils.getSubject(); try { subject.login(token); return "index"; } catch (UnknownAccountException e){ System.out.println("UnknownAccountException:"+e.getMessage()); } catch (IncorrectCredentialsException e){ System.out.println("IncorrectCredentialsException:"+e.getMessage()); } return "redirect:/user/login/page"; } @RequestMapping("/regist/page") public String userRegistPage(){ System.out.println("User regist page..."); return "WEB-INF/regist"; } @RequestMapping("/regist/logic") public String userRegistLogic(User user){ System.out.println("User regist logic..."); userService.createUser(user); return "redirect:/user/login/page"; } }
3.5.6 自定义Realm
-
Realm的职责是为shiro加载用户,角色和权限数据以提供shiro内部校验
-
有一定需要明确若自定义的Realm只负责身份校验,可以继承:AuthenticatingRealm
-
若自定义的Realm类不仅要负责身份校验,还要负责权限校验,那就要继承:AuthorizingRealm
MyRealm.java
package com.shiro.realm; import com.shiro.bean.User; import com.shiro.service.PermissionService; import com.shiro.service.RoleService; import com.shiro.service.UserService; import org.apache.shiro.authc.*; 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.apache.shiro.util.ByteSource; import org.springframework.web.context.ContextLoader; import org.springframework.web.context.WebApplicationContext; import java.util.Set; public class MyRealm extends AuthorizingRealm { // 是否支持token(包含username和password) public boolean supports(AuthenticationToken token) { if (token instanceof UsernamePasswordToken){ return true; } return false; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 1.获取username String username = (String) token.getPrincipal(); System.out.println("username:"+username); // 2.查询用户身份信息 // 2.1 获取Spring工厂 WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext(); UserService userService = (UserService) context.getBean("userServiceImpl"); User user = userService.queryUserByUsername(username); // 3.将用户信息封装成一个AuthenticationInfo if (user!=null) { // 加密处理过的 SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName()); return info; } return null; } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 1.获取username String username = (String) principals.getPrimaryPrincipal(); // 2.获取Spring工厂 WebApplicationContext context = ContextLoader.getCurrentWebApplicationContext(); RoleService roleService = (RoleService) context.getBean("roleServiceImpl"); PermissionService permissionService = (PermissionService) context.getBean("permissionServiceImpl"); Set<String> roles = roleService.queryRoleByUsername(username); Set<String> permissions = permissionService.queryPermissionByUsername(username); for (String role : roles) { System.out.print(role+"/"); } System.out.println(); for (String permission : permissions) { System.out.print(permission+"/"); } // 3.将查询到的roles和permissions封装 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.setRoles(roles); info.setStringPermissions(permissions); return info; } }
3.6 Tomcat+Run
- 配置好Tomcat,然后运行,进行一下各种校验测试.
- 这个就不用贴了吧~贴了太侮辱智商了
3.7 Over
感谢阅读,希望能对您有帮助,有问题请在留言中指出,祝您生活愉快~
求赞~