学习目标#认识 Shiro 的整体架构,各组件的概念Shiro 认证,授权的过程Shiro 自定义的 Realm,FilterShiro Session 管理Shiro 缓存管理Shiro 集成 SpringShiro 简介#Apache 的强大灵活的开源安全框架认证、授权、企业会话管理、安全加密Shiro 与 Spring Security 比较#Shiro#简单、灵活可脱离 Spring粒度较粗Spring Security#复杂、笨重不可脱离 Spring粒度更细Shiro 整体架构#
Shiro 认证和授权#认证过程#创建 SecurityManager -> 主体提交认证 -> SecurityManager 认证 -> Authenticator 认证 -> Realm 验证授权过程#创建 SecurityManager -> 主体提交授权 -> SecurityManager 授权 -> Authorizer 授权 -> Realm 获取角色权限数据测试代码如下(这里先使用 SimpleAccountRealm 作为 Realm):
public class AuthenticationTest {
private final SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
@Before
public void addUser() {
simpleAccountRealm.addAccount("Wxk", "123456", "admin", "user");
}
@Test
public void testAuthentication() {
// 1. 构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 2. 给SecurityManager设置Realm
defaultSecurityManager.setRealm(simpleAccountRealm);
// 3. 给SecurityUtils设置SecurityManager
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 4. 获取Subject
Subject subject = SecurityUtils.getSubject();
// 5. Subject提交认证请求
UsernamePasswordToken token = new UsernamePasswordToken("Wxk", "123456");
subject.login(token);
/*
认证
*/
System.out.println("isAuthenticated: " + subject.isAuthenticated());
/*
授权
*/
subject.checkRoles("admin", "user");
}
}
Realm#内置 RealmIniRealmJdbcRealm自定义 RealmIniRealm#在 resources 下创建一个 user.ini 文件:[users]
Wxk=123456,admin
[roles]
admin=user:delete
使用 IniRealm 作为 Realm测试代码如下:
public class IniRealmTest {
private final IniRealm iniRealm = new IniRealm("classpath:user.ini");
@Test
public void testAuthentication() {
// 1. 构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 2. 给SecurityManager设置Realm
defaultSecurityManager.setRealm(iniRealm);
// 3. 给SecurityUtils设置SecurityManager
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 4. 获取Subject
Subject subject = SecurityUtils.getSubject();
// 5. Subject提交认证请求
UsernamePasswordToken token = new UsernamePasswordToken("Wxk", "123456");
subject.login(token);
/*
认证
*/
System.out.println("isAuthenticated: " + subject.isAuthenticated());
/*
授权
*/
// 检查角色授权
subject.checkRole("admin");
// 检查权限授权
subject.checkPermission("user:delete");
}
}
JdbcRealm#分别创建数据库表:用户表 test_user, 用户角色表 test_user_roles, 用户权限表 test_users_permissions。给 JdbcRealm 设置数据源打开权限查询开关给 JdbcRealm 自定义认证、角色和权限SQL查询语句使用 JdbcRealm 作为 Realm数据库表如下:
测试代码如下:
public class JdbcRealmTest {
private final DruidDataSource dataSource = new DruidDataSource();
private final JdbcRealm jdbcRealm = new JdbcRealm();
@Test
public void testAuthentication() {
// 设置数据源
dataSource.setUrl("jdbc:mysql://localhost:3306/test");
dataSource.setUsername("root");
dataSource.setPassword("123456");
jdbcRealm.setDataSource(dataSource);
// 打开权限查询开关
jdbcRealm.setPermissionsLookupEnabled(true);
// 自定义认证查询SQL
String AuthenticationSQL = "SELECT password FROM test_user WHERE username = ?";
jdbcRealm.setAuthenticationQuery(AuthenticationSQL);
// 自定义角色查询SQL
String rolesSQL = "SELECT role_name FROM test_user_roles WHERE username = ?";
jdbcRealm.setUserRolesQuery(rolesSQL);
// 自定义权限查询SQL
String permissionsSQL = "SELECT permission FROM test_roles_permissions WHERE role_name = ?";
jdbcRealm.setPermissionsQuery(permissionsSQL);
// 1. 构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 2. 给SecurityManager设置Realm
defaultSecurityManager.setRealm(jdbcRealm);
// 3. 给SecurityUtils设置SecurityManager
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 4. 获取Subject
Subject subject = SecurityUtils.getSubject();
// 5. Subject提交认证请求
UsernamePasswordToken token = new UsernamePasswordToken("Wxk", "123456");
subject.login(token);
/*
认证
*/
System.out.println("isAuthenticated: " + subject.isAuthenticated());
/*
授权
*/
// 检查角色授权
subject.checkRoles("admin", "user");
// 检查权限授权
subject.checkPermission("user:delete");
}
}
自定义 Realm#自定义 MyRealm 类继承自 AuthorizingRealm,重写 doGetAuthenticationInfo 和 doGetAuthorizationInfo 方法来返回认证和授权验证信息:public class MyRealm extends AuthorizingRealm {
/**
* 模拟数据库或缓存
*/
Map
put("Wxk", "123456");
}};
{
// 设置Realm名称
super.setName("myRealm");
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 1. 获得用户名
String username = (String) principals.getPrimaryPrincipal();
// 2. 通过用户名到数据库或缓存中获取角色集合和权限集合
Set
Set
// 3. 给AuthorizationInfo设置角色集合和权限结合
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setRoles(roles);
simpleAuthorizationInfo.setStringPermissions(permissions);
// 4. 返回AuthorizationInfo
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 1. 从Subject传来的认证信息中获得用户名
String username = (String) token.getPrincipal();
// 2. 通过用户名到数据库中获取凭证
String password = getPasswordByUsername(username);
if (password == null) {
return null;
}
// 3. 返回AuthenticationInfo(将会和token进行匹配)
return new SimpleAuthenticationInfo(username, password, getName());
}
/**
* 模拟数据库查询密码
* @param username 用户名
* @return 密码
*/
private String getPasswordByUsername(String username) {
return userMap.get(username);
}
/**
* 模拟数据库或缓存查询角色集合
* @param username 用户名
* @return 角色集合
*/
private Set
Set
roles.add("admin");
roles.add("user");
return roles;
}
/**
* 模拟数据库或缓存查询权限集合
* @param username 用户名
* @return 权限集合
*/
private Set
Set
permissions.add("user:delete");
return permissions;
}
}
测试代码:public class MyRealmTest {
private final MyRealm myRealm = new MyRealm();
@Test
public void testAuthentication() {
// 1. 构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 2. 给SecurityManager设置Realm
defaultSecurityManager.setRealm(myRealm);
// 3. 给SecurityUtils设置SecurityManager
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 4. 获取Subject
Subject subject = SecurityUtils.getSubject();
// 5. Subject提交认证请求
UsernamePasswordToken token = new UsernamePasswordToken("Wxk", "123456");
subject.login(token);
/*
认证
*/
System.out.println("isAuthenticated: " + subject.isAuthenticated());
/*
授权
*/
// 检查角色授权
subject.checkRoles("admin", "user");
// 检查权限授权
subject.checkPermission("user:delete");
}
}
Shiro 加密#Shiro 散列配置HashedCredentialsMatcher自定义 Realm 中使用散列盐的使用数据库中的密码加盐后 md5 哈希加密。在 Realm 返回 AuthenticationInfo 前给其设置盐值:SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, getName());
// 设置盐值
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("salt"));
return simpleAuthenticationInfo;
进行认证时,给 Realm 设置 HashedCredentialsMatcher。// 给Realm设置HashedCredentialsMatcher
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
// 哈希算法
matcher.setHashAlgorithmName("md5");
// 哈希次数
matcher.setHashIterations(1);
myRealm.setCredentialsMatcher(matcher);
完整 MyRealm 代码:
public class MyRealm extends AuthorizingRealm {
/**
* 模拟数据库或缓存
*/
Map
put("Wxk", "f51703256a38e6bab3d9410a070c32ea");
}};
{
// 设置Realm名称
super.setName("myRealm");
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
// 1. 获得用户名
String username = (String) principals.getPrimaryPrincipal();
// 2. 通过用户名到数据库或缓存中获取角色集合和权限集合
Set
Set
// 3. 给AuthorizationInfo设置角色集合和权限结合
SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
simpleAuthorizationInfo.setRoles(roles);
simpleAuthorizationInfo.setStringPermissions(permissions);
// 4. 返回AuthorizationInfo
return simpleAuthorizationInfo;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
// 1. 从Subject传来的认证信息中获得用户名
String username = (String) token.getPrincipal();
// 2. 通过用户名到数据库中获取凭证
String password = getPasswordByUsername(username);
if (password == null) {
return null;
}
// 3. 返回AuthenticationInfo(将会和token进行匹配)
SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(username, password, getName());
// 设置盐值
simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("salt"));
return simpleAuthenticationInfo;
}
/**
* 模拟数据库查询密码
* @param username 用户名
* @return 密码
*/
private String getPasswordByUsername(String username) {
return userMap.get(username);
}
/**
* 模拟数据库或缓存查询角色集合
* @param username 用户名
* @return 角色集合
*/
private Set
Set
roles.add("admin");
roles.add("user");
return roles;
}
/**
* 模拟数据库或缓存查询权限集合
* @param username 用户名
* @return 权限集合
*/
private Set
Set
permissions.add("user:delete");
return permissions;
}
public static void main(String[] args) {
Md5Hash md5Hash = new Md5Hash("123456", "salt");
System.out.println(md5Hash);
}
}
完整测试代码:
public class MyRealmTest {
private final MyRealm myRealm = new MyRealm();
@Test
public void testAuthentication() {
// 1. 构建SecurityManager环境
DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
// 2. 给SecurityManager设置Realm
defaultSecurityManager.setRealm(myRealm);
// 给Realm设置HashedCredentialsMatcher
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
// 哈希算法
matcher.setHashAlgorithmName("md5");
// 哈希次数
matcher.setHashIterations(1);
myRealm.setCredentialsMatcher(matcher);
// 3. 给SecurityUtils设置SecurityManager
SecurityUtils.setSecurityManager(defaultSecurityManager);
// 4. 获取Subject
Subject subject = SecurityUtils.getSubject();
// 5. Subject提交认证请求
UsernamePasswordToken token = new UsernamePasswordToken("Wxk", "123456");
subject.login(token);
/*
认证
*/
System.out.println("isAuthenticated: " + subject.isAuthenticated());
/*
授权
*/
// 检查角色授权
subject.checkRoles("admin", "user");
// 检查权限授权
subject.checkPermission("user:delete");
}
}
Shiro 过滤器#anon, authBasic, authc, user, logoutperms, roles, ssl, port自定义 Filter#如创建 RolesOrFilter 继承自 AuthorizationFilter,过滤条件为满足 roles 集合其中任何一个即可::
public class RolesOrFilter extends AuthorizationFilter {
@Override
protected boolean isAccessAllowed(javax.servlet.ServletRequest servletRequest, javax.servlet.ServletResponse servletResponse, Object o) throws Exception {
Subject subject = getSubject(servletRequest, servletResponse);
String[] roles = (String[]) o;
if (roles == null || roles.length == 0) {
return true;
}
for (String role : roles) {
if (subject.hasRole(role)) {
return true;
}
}
return false;
}
}
Shiro 会话管理#SessionManager、SessionDAORedis 实现 Session 共享1. SessionDAO#实现自定义的 SessionDao 继承自 AbstractSessionDAO:
public class RedisSessionDao extends AbstractSessionDAO {
@Resource
private JedisUtil jedisUtil;
private final static String SHIRO_SESSION_PREFIX = "session:";
private byte[] getKey(String key) {
return (SHIRO_SESSION_PREFIX + key).getBytes();
}
private void saveSession(Session session) {
if (session != null && session.getId() != null) {
byte[] key = getKey(session.getId().toString());
byte[] value = SerializationUtils.serialize(session);
jedisUtil.set(key, value);
jedisUtil.expire(key, 600);
}
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = generateSessionId(session);
assignSessionId(session, sessionId);
saveSession(session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable sessionId) {
if (sessionId == null) {
return null;
}
byte[] key = getKey(sessionId.toString());
byte[] value = jedisUtil.get(key);
return (Session) SerializationUtils.deserialize(value);
}
@Override
public void update(Session session) throws UnknownSessionException {
saveSession(session);
}
@Override
public void delete(Session session) {
if (session == null || session.getId() == null) {
return;
}
byte[] key = getKey(session.getId().toString());
jedisUtil.del(key);
}
@Override
public Collection
Set
Set
if (CollectionUtils.isEmpty(keys)) {
return sessions;
}
for (byte[] key : keys) {
Session session = (Session) SerializationUtils.deserialize(jedisUtil.get(key));
sessions.add(session);
}
return sessions;
}
}
2. SessionManager#实现自定义的 SessionManager 继承自 DefaultSessionManager:
public class MySessionManager extends DefaultWebSessionManager {
@Override
protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {
Serializable sessionId = getSessionId(sessionKey);
if (sessionId == null) {
return null;
}
ServletRequest request = null;
if (sessionKey instanceof WebSessionKey) {
request = ((WebSessionKey) sessionKey).getServletRequest();
}
Session session = null;
if (request != null) {
// 先尝试从request中获取,以减少从redis中获取的次数
session = (Session) request.getAttribute(sessionId.toString());
// 若从request获取不到,再按照自己重写的readSession()方法的方式获取,并存入request
if (session == null) {
session = super.retrieveSession(sessionKey);
request.setAttribute(sessionId.toString(), session);
}
}
if (session == null) {
String msg = "Could not find session with ID [" + sessionId + "]";
throw new UnknownSessionException(msg);
}
return session;
}
}
给 securityManager 设置 sessionManager。Shiro 缓存管理#CacheManager、CacheRedis 实现 CacheManager1. RedisCache#自定义 RedisCache 继承自 Cache:
public class RedisCache
@Resource
private JedisUtil jedisUtil;
private final static String CACHE_PREFIX = "cache:";
private byte[] getKey(K k) {
if (k instanceof String) {
return (CACHE_PREFIX + k).getBytes();
}
return SerializationUtils.serialize(k);
}
@Override
public V get(K k) throws CacheException {
byte[] value = jedisUtil.get(getKey(k));
if (value != null) {
return (V) SerializationUtils.deserialize(value);
}
return null;
}
@Override
public V put(K k, V v) throws CacheException {
byte[] key = getKey(k);
byte[] value = SerializationUtils.serialize(v);
jedisUtil.set(key, value);
jedisUtil.expire(key, 600);
return v;
}
@Override
public V remove(K k) throws CacheException {
byte[] key = getKey(k);
byte[] value = jedisUtil.get(key);
jedisUtil.del(key);
if (value != null) {
return (V) SerializationUtils.deserialize(value);
}
return null;
}
@Override
public void clear() throws CacheException {
}
@Override
public int size() {
return 0;
}
@Override
public Set
return null;
}
@Override
public Collection
return null;
}
}
2. RedisCacheManager#自定义 RedisCacheManager 继承自 CacheManager:
public class RedisCacheManager implements CacheManager {
@Resource
private RedisCache redisCache;
@Override
public
return redisCache;
}
}
给 securityManager 设置 cacheManager。Shiro RememberMe#配置 Bean:CookieRememberMeManager 和 SimpleCookie。设置 CookieRememberMeManager 的 cookie 为 SimpleCookie。给 securityManager 设置 rememberMeManager。提交认证前,设置 rememberMe。token.setRememberMe(true);
subject.login(token);