跟我学Shiro实践-简单的认证授权
Posted laoyeye
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了跟我学Shiro实践-简单的认证授权相关的知识,希望对你有一定的参考价值。
本文是基于张开涛老师的跟我学Shiro系列的个人实践。几个月前过了一遍张开涛的跟我学Shiro系列,因为没有实践,基本上又全部还给开涛老师了。趁着假期,这次准备将开涛老师的讲解实践一遍。当然本人的实践不会与开涛的实例完全相同,不然就没必要在写一遍了。
本文只会对相关必要的Shiro概念说明下,建议有时间可以阅读下开涛的Shiro系列:http://jinnianshilongnian.iteye.com/blog/2018398
Shiro的架构和简单的认证授权
1.1 Shiro架构和组件介绍
Apache Shiro是Java的一个权限安全框架,与之类似的还有Spring Security,如果只是简单应用的话我还是比较推荐Shiro的,因为Spring Security的集成我总感觉是太过复杂。
Subject:主体,可以看到主体可以是任何可以与应用交互的“用户”;
SecurityManager:相当于SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心脏;所有具体的交互都通过SecurityManager进行控制;它管理着所有Subject、且负责进行认证和授权、及会话、缓存的管理。
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得Shiro默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
Realm:可以有1个或多个Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是JDBC实现,也可以是LDAP实现,或者内存实现等等;由用户提供;注意:Shiro不知道你的用户/权限存储在哪及以何种格式存储;所以我们一般在应用中都需要实现自己的Realm;
SessionManager:如果写过Servlet就应该知道Session的概念,Session呢需要有人去管理它的生命周期,这个组件就是SessionManager;而Shiro并不仅仅可以用在Web环境,也可以用在如普通的JavaSE环境、EJB等环境;所有呢,Shiro就抽象了一个自己的Session来管理主体与应用之间交互的数据;这样的话,比如我们在Web环境用,刚开始是一台Web服务器;接着又上了台EJB服务器;这时想把两台服务器的会话数据放到一个地方,这个时候就可以实现自己的分布式会话(如把数据放到Memcached服务器);
SessionDAO:DAO大家都用过,数据访问对象,用于会话的CRUD,比如我们想把Session保存到数据库,那么可以实现自己的SessionDAO,通过如JDBC写到数据库;比如想把Session放到Memcached中,可以实现自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache进行缓存,以提高性能;
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
Cryptography:密码模块,Shiro提高了一些常见的加密组件用于如密码加密/解密的。
1.2 认证和授权的流程
认证:
1、创建SecurityManager环境
2、Subject主体提交认证
3、SecurityManager认证
4、Authenticator认证器认证
5、Realm验证
授权:
1、创建SecurityManager环境
2、Subject主体提交授权
3、SecurityManager授权
4、Authrizer授权
5、Realm访问数据库获取权限数据
maven依赖
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.9</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> </dependencies>
1.3 SimpleAccountRealm认证代码
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.SimpleAccountRealm; import org.apache.shiro.subject.Subject; import org.junit.Before; import org.junit.Test; public class LoginTest { SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm(); @Before public void addUser() { simpleAccountRealm.addAccount("laoyeye", "123456"); } @Test public void testAuthentication() { // 1.构建SecurityManager环境 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(simpleAccountRealm); // 2.Subject主体提交认证 SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("laoyeye", "123456"); subject.login(token); System.out.println("是否登录:" + subject.isAuthenticated()); subject.logout(); System.out.println("是否登录:" + subject.isAuthenticated()); } }
结果:
是否登录:true 是否登录:false
我们上面在上面流程中说有五步,代码中怎么只看到三步就登陆成功了呢,这里我们看下源码中内容就容易理解了。
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); Subject subject = securityManager.login(this, token); PrincipalCollection principals; String host = null; if (subject instanceof DelegatingSubject) { DelegatingSubject delegating = (DelegatingSubject) subject; //we have to do this in case there are assumed identities - we don‘t want to lose the ‘real‘ principals: principals = delegating.principals; host = delegating.host; } else { principals = subject.getPrincipals(); } if (principals == null || principals.isEmpty()) { String msg = "Principals returned from securityManager.login( token ) returned a null or " + "empty value. This value must be non null and populated with one or more elements."; throw new IllegalStateException(msg); } this.principals = principals; this.authenticated = true; if (token instanceof HostAuthenticationToken) { host = ((HostAuthenticationToken) token).getHost(); } if (host != null) { this.host = host; } Session session = subject.getSession(false); if (session != null) { this.session = decorate(session); } else { this.session = null; } }
首先我们的主体subject.login(token)的时候提交了认证,SecurityManager开始进入认证,然后进入到下面这里。
public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { return this.authenticator.authenticate(token); }
Authenticator认证器开始认证工作。接着往下走。。
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); if (realms.size() == 1) { return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { return doMultiRealmAuthentication(realms, authenticationToken); } }
可以看到我们的Authenticator开始去取我们代码刚开始设置的Realm数据了,然整个流程就走通了。
授权
接着上面的代码,我们添加下面红色标注的一行代码
public class LoginTest { SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm(); @Before public void addUser() { simpleAccountRealm.addAccount("laoyeye", "123456","admin"); } @Test public void testAuthentication() { // 1.构建SecurityManager环境 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(simpleAccountRealm); // 2.Subject主体提交认证 SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("laoyeye", "123456"); subject.login(token); System.out.println("是否登录:" + subject.isAuthenticated()); subject.checkRoles("admin"); //授权 subject.logout(); System.out.println("是否登录:" + subject.isAuthenticated()); } }
结果:
org.apache.shiro.authz.UnauthorizedException: Subject does not have role [admin]
at org.apache.shiro.authz.ModularRealmAuthorizer.checkRole(ModularRealmAuthorizer.java:421)
at org.apache.shiro.authz.ModularRealmAuthorizer.checkRoles(ModularRealmAuthorizer.java:440)
at org.apache.shiro.mgt.AuthorizingSecurityManager.checkRoles(AuthorizingSecurityManager.java:173)
at org.apache.shiro.subject.support.DelegatingSubject.checkRoles(DelegatingSubject.java:250)
at com.allan.test.LoginTest.testAuthentication(LoginTest.java:33)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:606)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:69)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:48)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
at org.junit.runners.ParentRunner.run(ParentRunner.java:292)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)
报错说没有admin用户,这时候我们在
simpleAccountRealm.addAccount("laoyeye", "123456","admin");
就正常了。
授权的流程基本上和认证差不多,大家可以自行看下源码。
1.4 JdbcRealm的实现
maven依赖
<dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.9</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.4.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.25</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>0.2.23</version> </dependency> </dependencies>
import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.mgt.DefaultSecurityManager; import org.apache.shiro.realm.jdbc.JdbcRealm; import org.apache.shiro.subject.Subject; import org.junit.Test; public class JdbcRealmTest { DruidDataSource dataSource = new DruidDataSource(); { dataSource.setUrl("jdbc:mysql://xxxxxx:3306/shiro"); dataSource.setUsername("root"); dataSource.setPassword("xxxx"); } @Test public void testAuthentication() { JdbcRealm jdbcRealm = new JdbcRealm(); jdbcRealm.setDataSource(dataSource); //开启角色权限 jdbcRealm.setPermissionsLookupEnabled(true); //自定义SQL //认证查询 jdbcRealm.setAuthenticationQuery("select password from test_users where username = ?"); //用户角色查询 jdbcRealm.setUserRolesQuery("select role_name from test_user_roles where username = ?"); // 1.构建SecurityManager环境 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(jdbcRealm); // 2.主题提交认证请求 SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("laoyeye", "123456"); subject.login(token); System.out.println("是否登录:" + subject.isAuthenticated()); subject.checkRoles("admin","user"); //授权 subject.checkPermission("user:select"); //权限 subject.logout(); System.out.println("是否登录:" + subject.isAuthenticated()); } }
1.5 自定义Realm的实现
public class CustomRealm extends AuthorizingRealm { Map<String, String> userMap = new HashMap<>(); //模拟数据库或缓存的数据 { // Md5Hash md5 = new Md5Hash("123456"); //加密 Md5Hash md5 = new Md5Hash("123456","laoyeyesalt");//加盐 userMap.put("laoyeye", md5.toString()); // userMap.put("laoyeye", "123456"); super.setName("customRealm"); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); // 从数据库或者缓存中获得角色数据 Set<String> roles = getRolesByUserName(username); Set<String> permissions = getPermissionsByUserName(username); SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); simpleAuthorizationInfo.setStringPermissions(permissions); simpleAuthorizationInfo.setRoles(roles); return simpleAuthorizationInfo; } private Set<String> getPermissionsByUserName(String username) { Set<String> sets = new HashSet<>(); sets.add("user:delete"); sets.add("user:add"); return sets; } private Set<String> getRolesByUserName(String username) { Set<String> sets = new HashSet<>(); sets.add("admin"); sets.add("user"); return sets; } @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 1.从主体传过来的认证信息中,获得用户名 String username = (String) token.getPrincipal(); // 2.通过用户名到数据库中获取凭证 String password = getPasswordByUsername(username); if(password == null) { return null; } SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("laoyeye", password, "customRealm"); //加盐 simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("laoyeyesalt")); return simpleAuthenticationInfo; } private String getPasswordByUsername(String username) { return userMap.get(username); } }
public class CustomRealmTest { CustomRealm customRealm = new CustomRealm(); @Test public void testAuthentication() { // 1.构建SecurityManager环境 DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager(); defaultSecurityManager.setRealm(customRealm); //声明CustomRealm使用了Md5加密 HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); matcher.setHashAlgorithmName("md5"); matcher.setHashIterations(1); customRealm.setCredentialsMatcher(matcher); // 2.主题提交认证请求 SecurityUtils.setSecurityManager(defaultSecurityManager); Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("laoyeye", "123456"); subject.login(token); System.out.println("是否登录:" + subject.isAuthenticated()); subject.checkRoles("admin"); //授权 subject.checkPermission("user:delete"); subject.logout(); System.out.println("是否登录:" + subject.isAuthenticated()); } }
以上是关于跟我学Shiro实践-简单的认证授权的主要内容,如果未能解决你的问题,请参考以下文章
SpringBoot实现基于shiro安全框架的,配合thymeleaf模板引擎的用户认证和授权