基本上涉及到用户参与的系统都要进行权限管理,权限管理属于系统安全的范畴,权限管理实现对用户访问系统的控制,按照安全规则或安全策略控制用户可以访问并且只能访问被授权的资源。
身份认证 就是判断一个用户是否为合法用户的过程,最简单方式就是根据用户输入的用户名和密码,和系统存储层一致不,来判断用户身份是否正确。
身份授权 即访问控制,控制谁能访问那些资源,不同的用户应该拥有不同的资源访问权限,常见的有学校的教务管理系统:有教师,学生,管理员登录几个模块,以不同的身份登录就会显示不同的界面!
shiro是Apache旗下的一个开源框架,它将软件系统的安全认证相关的功能抽取出来,是一个功能强大且简单易用的Java安全框架,它可以用来进行身份验证,授权,加密和会话管理。
Subject即主体,外部应用与Subject进行交互,Subject对象记录了当前操作的用户,将用户的概念理解成当前操作的主体,可能是一个浏览器的请求的用户,也可能是一个允许的程序。Subject在shiro中是一个接口,接口中定义了很多认证授权的方法,外部程序通过Subject对象进行认证和授权,而Subject对象是通过SecurityManager进行认证和授权!
SecurityManager即安全管理器,对全部的Subject对象进行安全管理,它是shiro的核心,通过SecurityManager可以完成对Subject对象的认证授权等系列操作,实质上是使用Authenticator进行认证,使用Authorizer进行授权,使用SessionManager进行会话管理!
Authenticator即认证器,对用户身份进行认证,Authenticator是一个接口,shiro提供了ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足认证的大部分需求,也可以使用自定义的认证器。
Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否由此功能的操作权限。
Realm即领域,相当于datasource,SecurityManager进行安全认证是需要从Realm获取用户权限数据,比如:用户的身份信息如果放到数据库中,那么就需要从数据库中获取到用户身份信息。
注:不要把Realm认为只是在里面取数据,在Realm还有认证和授权相关的代码!
SessionManager即会话管理,shiro框架定义了一系列会话管理,它不依赖于web的session,所以shiro可以使用在非web的环境下,也可以将分布式的应用集中在一起管理,此特性可以实现单点登录!
SessionDao即会话dao,是对session会话操作的一套接口,比如将session存储到数据库,也可以使用jdbc将session存入数据库。
CacheManager即缓存管理器,将用户权限数据存储到缓存中,可以提高性能。
Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发,比如提供了常用分散列,盐值计算,加/解密等功能!
认证就是判断一个用户是否为合法用户的过程,最简单方式就是根据用户输入的用户名和密码,和系统存储层一致不,来判断用户身份是否正确。
访问系统的用户,主体可以是程序,用户等,进行认证的都称为主体。
Principal:身份信息是主体进行身份认证的标识,标识必须具有唯一性,如电话号码,手机号,邮箱地址,一个主体可以有多个身份,但必须有一个主身份!
credential:凭证信息只有主体自己知道的安全信息,如密码,证书等。
使用本地shiro.ini文件开发
创建maven项目引入依赖 <!--引入shiro相关依赖--> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.5.3</version> </dependency> 引入shiro.ini配置文件 开发认证代码 /** * 测试认证(加载本地的realm) * */ public class TestAuthenticator { public static void main( String[] args ) { //1.创建SecurityManager对象 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(new IniRealm("classpath:shiro.ini")); //可以从数据库中获取realm //2.将安装工具类设置为默认的安全管理器 SecurityUtils.setSecurityManager(defaultSecurityManager); //3.获取主体对象 Subject subject = SecurityUtils.getSubject(); //4.创建token令牌 UsernamePasswordToken token = new UsernamePasswordToken("liuzeyu","809080"); //5.执行登录方法 try { subject.login(token); System.out.println("登陆成功!!"); }catch (UnknownAccountException e){ e.printStackTrace(); System.out.println("用户名错误!!"); }catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("密码错误!!"); } } }使用本地shiro.ini开发,使用的realm就是我们本地提供的,而实际开发中,realm的数据往往是我们的数据库中读取!
shiro提供的Realm
Realm的实现类中认证是使用SimpleAccountRealm SimpleAccountRealm 部分源码中有两个方法,分别是认证:doGetAuthenticationInfo(AuthenticationToken token),授权:doGetAuthorizationInfo(PrincipalCollection principals)
//判断用户是否为合法用户 protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken)token; SimpleAccount account = this.getUser(upToken.getUsername()); if (account != null) { if (account.isLocked()) { throw new LockedAccountException("Account [" + account + "] is locked."); } if (account.isCredentialsExpired()) { String msg = "The credentials for account [" + account + "] are expired"; throw new ExpiredCredentialsException(msg); } } return account; } //为合法用户授权 protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = this.getUsername(principals); this.USERS_LOCK.readLock().lock(); AuthorizationInfo var3; try { var3 = (AuthorizationInfo)this.users.get(username); } finally { this.USERS_LOCK.readLock().unlock(); } return var3; } 自定义Realm /** * 自定义realm,分别实现认证和授权方法 */ public class CustomerRealm extends AuthorizingRealm { //身份认证 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { return null; } //授权 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { //获取用户名key String principal = (String) authenticationToken.getPrincipal(); //根据前台传递过来的principal身份信息去使用jdbc或者mybatis查询数据库 if("liuzeyu".equals(principal)){ /** * principal:返回当前数据库中的正确用户名 * credentials:返回当前数据库中正确的密码 * realmName:当前realm名字 */ return new SimpleAuthenticationInfo(principal,"809080",this.getName()); } return null; } } 使用自定义Realm进行认证 /** * 测试加载远程数据库的realm */ public class TestCustomerAuthenticator { public static void main(String[] args) { //1.创建SecurityManager DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(new CustomerRealm()); //2.将安装工具设置为默认的安全管理器 SecurityUtils.setSecurityManager(defaultSecurityManager); //3.获取主体对象 Subject subject = SecurityUtils.getSubject(); //4.获取令牌 UsernamePasswordToken token = new UsernamePasswordToken("liuzeyu", "809080"); //5.执行 try { subject.login(token); System.out.println("登陆成功!!"); }catch (UnknownAccountException e){ e.printStackTrace(); System.out.println("用户名错误"); }catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("密码错误"); } } }在自定义的Realm基础上加入授权功能,只有通过认证用户才可以被授权!
自定义Realm public class CustomerReleamMd5 extends AuthorizingRealm { @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { String primaryPrincipal = (String) principalCollection.getPrimaryPrincipal(); System.out.println(primaryPrincipal); System.out.println("primaryPrincipal:"+primaryPrincipal); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.addRole("admin"); //添加角色admin simpleAuthorizationInfo.addStringPermission("product:*:01"); //权限字符串 product 可以对01实例执行任何操作 return simpleAuthorizationInfo; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { String principal = (String) authenticationToken.getPrincipal(); //利用假数据去匹配 if("liuzeyu".equals(principal)){ return new SimpleAuthenticationInfo(principal, "641acb1f58ed7e24677a1e26d2497604", //传递不同的密码强度验证 ByteSource.Util.bytes("Xq*07"), this.getName()); } return null; } } 测试 public class TestCustomerMd5Authenticator { public static void main(String[] args) { //身份验证 //1.1创建安全管理器 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); //设置自定义realm获取认证数据 CustomerReleamMd5 releamMd5 = new CustomerReleamMd5(); //创建凭证匹配器 HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5"); //(md5)算法 hashedCredentialsMatcher.setHashIterations(1024); //散列1024次,默认是一次 releamMd5.setCredentialsMatcher(hashedCredentialsMatcher); //为凭证设置凭证适配器(hash) defaultSecurityManager.setRealm(releamMd5); //1.2设置安全管理器 SecurityUtils.setSecurityManager(defaultSecurityManager); //1.3获取主体 Subject subject = SecurityUtils.getSubject(); //1.4获取令牌 UsernamePasswordToken token = new UsernamePasswordToken("liuzeyu","809080"); //1.5执行 try { subject.login(token); System.out.println("登陆成功"); }catch (UnknownAccountException e){ e.printStackTrace(); System.out.println("用户名错误"); }catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("密码错误"); } //2.授权 //如果认证通过 if(subject.isAuthenticated()){ //基于角色管理 boolean admin = subject.hasRole("admin"); System.out.println(admin); //基于资源管理 boolean permitted = subject.isPermitted("product:create:01"); System.out.println(permitted); } } }模板引擎使用原生的JSP,配合ShiroFilter实现用户的认证和授权,实现用户注册,登录,可以以不同的身份显示不同的资源!
表和表之间的关系:
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.liuzeyu.mapper.UserMapper"> <insert id="save" useGeneratedKeys="true" parameterType="user" keyProperty="id" > insert into t_user values(#{id},#{username},#{password},#{salt}) </insert> <select id="findByUsername" parameterType="String" resultType="user"> select * from t_user where username=#{username} </select> <!--查询的是一个用户对应的多个角色信息--> <resultMap id="userMap" type="User"> <id property="id" column="id"/> <result property="username" column="username"/> <collection property="roles" javaType="list" ofType="Role"> <id column="rid" property="id"/> <result column="name" property="name"/> </collection> </resultMap> <select id="findRolesByUsername" parameterType="String" resultMap="userMap"> SELECT u.id,u.username,r.id rid,r.name from t_user u left join t_user_role ur on u.id = ur.userid left join t_role r on r.id = ur.roleid where u.username = #{username} </select> <select id="findPermsByRoleId" parameterType="Integer" resultType="Perms"> select p.id,p.name,r.name from t_role r left join t_role_perms rp on rp.roleid=r.id left join t_perms p on p.id = rp.permsid where r.id=#{id} </select> </mapper> service层开发 public interface UserService { public void save(User user); public User findByUsername(String username); public User findRolesByUsername(String username); public List<Perms> findPermsByRoleId(Integer id); } @Service @Transactional public class UserServiceImpl implements UserService { @Autowired private UserMapper mapper; @Override public void save(User user) { //获取盐 String salt = SaltUtils.getSalt(8); user.setSalt(salt); Md5Hash md5Hash = new Md5Hash(user.getPassword(), salt, 1024); user.setPassword(md5Hash.toHex()); mapper.save(user); } @Override public User findByUsername(String username) { return mapper.findByUsername(username); } @Override public User findRolesByUsername(String username) { return mapper.findRolesByUsername(username); } @Override public List<Perms> findPermsByRoleId(Integer id) { return mapper.findPermsByRoleId(id); } }login.jsp
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" language="java" %> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h2>登录页面</h2> <form action="${pageContext.request.contextPath}/user/login" method="post"> 账号:<input type="text" name="username"><br> 密码:<input type="text" name="password"><br> <input type="submit" value="登录"><br> </form> </body> </html>register.jsp
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" language="java" %> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h2>注册页面</h2> <form action="${pageContext.request.contextPath}/user/register" method="post"> 账号:<input type="text" name="username"><br> 密码:<input type="text" name="password"><br> <input type="submit" value="立即注册"><br> </form> </body> </html>index.jsp
<%@page contentType="text/html; UTF-8" pageEncoding="UTF-8" isELIgnored="false" language="java" %> <%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>系统主页</title> </head> <body> <h1>V0.1 系统主页</h1> <ul> <shiro:hasAnyRoles name="admin,user"> <li><a href="">用户管理</a> <ul> <ui> <shiro:hasPermission name="user:create:*"> <li>创建</li> </shiro:hasPermission> <shiro:hasPermission name="user:delete:01"> <li>删处</li> </shiro:hasPermission> <shiro:hasPermission name="user:update:01"> <li>修改</li> </shiro:hasPermission> <shiro:hasPermission name="user:select:01"> <li>查询</li> </shiro:hasPermission> </ui> </ul> </li> </shiro:hasAnyRoles> <shiro:hasAnyRoles name="admin,product,order"> <shiro:hasPermission name="product:*:01"> <li><a href="">商品管理</a></li> </shiro:hasPermission> <shiro:hasPermission name="order:*:01"> <li><a href="">订单管理</a></li> </shiro:hasPermission> <li><a href="">物流管理</a></li> </shiro:hasAnyRoles> </ul> <a href="${pageContext.request.contextPath}/user/logout">退出</a> </body> </html>改用实现ByteSource 接口,因为SimpleByteSource 没有提供默认的无参构造,序列化会出现问题!
//自定义salt实现,实现序列化接口 public class MyByteSource implements ByteSource , Serializable { private byte[] bytes; private String cachedHex; private String cachedBase64; public MyByteSource(){ } public MyByteSource(byte[] bytes) { this.bytes = bytes; } public MyByteSource(char[] chars) { this.bytes = CodecSupport.toBytes(chars); } public MyByteSource(String string) { this.bytes = CodecSupport.toBytes(string); } public MyByteSource(ByteSource source) { this.bytes = source.getBytes(); } public MyByteSource(File file) { this.bytes = (new MyByteSource.BytesHelper()).getBytes(file); } public MyByteSource(InputStream stream) { this.bytes = (new MyByteSource.BytesHelper()).getBytes(stream); } public static boolean isCompatible(Object o) { return o instanceof byte[] || o instanceof char[] || o instanceof String || o instanceof ByteSource || o instanceof File || o instanceof InputStream; } public byte[] getBytes() { return this.bytes; } public boolean isEmpty() { return this.bytes == null || this.bytes.length == 0; } public String toHex() { if (this.cachedHex == null) { this.cachedHex = Hex.encodeToString(this.getBytes()); } return this.cachedHex; } public String toBase64() { if (this.cachedBase64 == null) { this.cachedBase64 = Base64.encodeToString(this.getBytes()); } return this.cachedBase64; } public String toString() { return this.toBase64(); } public int hashCode() { return this.bytes != null && this.bytes.length != 0 ? Arrays.hashCode(this.bytes) : 0; } public boolean equals(Object o) { if (o == this) { return true; } else if (o instanceof ByteSource) { ByteSource bs = (ByteSource)o; return Arrays.equals(this.getBytes(), bs.getBytes()); } else { return false; } } private static final class BytesHelper extends CodecSupport { private BytesHelper() { } public byte[] getBytes(File file) { return this.toBytes(file); } public byte[] getBytes(InputStream stream) { return this.toBytes(stream); } } }在springboot整合Shiro的基础上,修改模板引擎为thymeleaf,需要注意的是thymeleaf界面的跳转要借助于控制器!
修改application.properties 将 spring.mvc.view.prefix=/ spring.mvc.view.suffix=.jsp修改为
spring.thymeleaf.cache=false spring.thymeleaf.suffix=.html spring.thymeleaf.prefix=classpath:/templates/ 添加springboot的thymeleaf的启动依赖和shiro整合thymeleaf <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>com.github.theborakompanioni</groupId> <artifactId>thymeleaf-extras-shiro</artifactId> <version>2.0.0</version> </dependency> 修改JSP页面为html页面index.html
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h2>登录页面</h2> <form th:action="@{/user/login}" method="post"> 账号:<input type="text" name="username"><br> 密码:<input type="text" name="password"><br> 请输入验证码:<input type="text" name="code"/> <input type="submit" value="登录"><br></form> <br><img th:src="@{/user/getImage}"> <a th:href="@{/user/registerview}">立即注册</a> </form> </body> </html>register.html
<!DOCTYPE html> <html lang="en" xmlns:th="http:www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Title</title> </head> <body> <h2>注册页面</h2> <form th:action="@{/user/register}" method="post"> 账号:<input type="text" name="username"><br> 密码:<input type="text" name="password"><br> <input type="submit" value="立即注册"><br> </form> </body> </html>index.html
<!DOCTYPE html > <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.pollix.at/thymeleaf/shiro" xmlns:shrio="http://www.pollix.at/thymeleaf/shiro"> <head> <meta charset="UTF-8"> <title>系统主页</title> </head> <body> <h1>V0.1 系统主页</h1> <h1><shiro:principal/></h1> <ul> <shiro:hasAnyRoles name="admin,user"> <li><a href="">用户管理</a> <ul> <ui> <shiro:hasPermission name="user:create:*"> <li>创建</li> </shiro:hasPermission> <shiro:hasPermission name="user:delete:01"> <li>删处</li> </shiro:hasPermission> <shiro:hasPermission name="user:update:01"> <li>修改</li> </shiro:hasPermission> <shiro:hasPermission name="user:select:01"> <li>查询</li> </shiro:hasPermission> </ui> </ul> </li> </shiro:hasAnyRoles> <shiro:hasAnyRoles name="admin,product,order"> <shiro:hasPermission name="product:*:01"> <li><a href="">商品管理</a></li> </shiro:hasPermission> <shiro:hasPermission name="order:*:01"> <li><a href="">订单管理</a></li> </shiro:hasPermission> <li><a href="">物流管理</a></li> </shiro:hasAnyRoles> </ul> <hr> <p><shiro:notAuthenticated> </shiro:notAuthenticated></p> <hr> <p><shrio:authenticated/></p> <a th:href="@{/user/logout}">退出</a> </body> </html> 修改web控制器 @Controller @RequestMapping("/user") public class UserController { @Autowired private UserService service; /** * 处理身份验证 * @param username * @param password * @return */ @PostMapping("/login") public String login(String username,String password,String code,HttpSession session){ String codes = (String) session.getAttribute("code"); try { if(codes.equalsIgnoreCase(code)){ Subject subject = SecurityUtils.getSubject(); subject.login(new UsernamePasswordToken(username,password)); System.out.println("登陆成功"); return "redirect:/user/index"; }else{ throw new RuntimeException("验证码错误"); } }catch (UnknownAccountException e){ e.printStackTrace(); System.out.println("用户名不存在"); }catch (IncorrectCredentialsException e){ e.printStackTrace(); System.out.println("密码错误"); }catch (RuntimeException e){ e.printStackTrace(); System.out.println(e.getMessage()); } return "redirect:/user/loginview"; } /** * 用户退出登录 * @return */ @GetMapping("logout") public String logout(){ Subject subject = SecurityUtils.getSubject(); subject.logout(); return "redirect:/user/loginview"; } @PostMapping("/register") public String register(User user){ try { service.save(user); System.out.println("注册成功"); return "redirect:/user/loginview"; }catch (Exception e){ e.printStackTrace(); System.out.println("注册失败"); return "redirect:/user/registerview "; } } @RequestMapping("/getImage") public void getImage(HttpSession session, HttpServletResponse response){ try { String verifyCode = VerifyCodeUtils.generateVerifyCode(4); session.setAttribute("code",verifyCode); ServletOutputStream os = response.getOutputStream(); //验证码存入图片 response.setContentType("image/png"); VerifyCodeUtils.outputImage(220, 60, os , verifyCode); } catch (IOException e) { e.printStackTrace(); } } @RequestMapping("/loginview") public String loginview(){ System.out.println("正在进入登录页面..."); return "login"; } @RequestMapping("/registerview") public String registerview(){ System.out.println("正在进入注册页面..."); return "register"; } @RequestMapping("/index") public String index(){ return "index"; } } 添加springboot方言处理,否则shiro标签将失效!在shiroConfig.java中添加
//加入spring boot的方言处理,否则页面标签将不起作用 @Bean(name = "shiroDialect") public ShiroDialect shiroDialect(){ return new ShiroDialect(); } 测试以xiaochen和zhangsan两种不同的身份登录系统!
xiaochen: zhangsan: 比对数据库中xiaochen和zhansan对象的角色信息和权限
根据中间表可以知道xiaochen:admin,zhangsan:user,product 测试再通过角色权限表查看: 由此可见,虽然zhangsan拥有两个角色,但是他并没有拥有订单(order)权限,所以zhangsan没有订单管理选项!!
测试完成!!
参考学习:https://www.bilibili.com/video/BV1uz4y197Zm?p=26