项目结构
pom.xml
xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <parent> <artifactId>DC.API_WEBartifactId> <groupId>com.hlinkgroupId> <version>1.0.1version> parent> <modelVersion>4.0.0modelVersion> <groupId>com.hlink.webgroupId> <artifactId>DC.WEB_EmpartifactId> <properties> <spring-version>5.0.5.RELEASspring-version> <shiro-version>1.4.0shiro-version> <slf4j-version>1.7.3slf4j-version> <kafka-version>2.11kafka-version> <java-version>1.8java-version> <junit-version>4.12junit-version> <jackson.version>2.9.4jackson.version> properties> <dependencies> <dependency> <groupId>com.hlinkgroupId> <artifactId>DC.API_CommonsartifactId> <version>1.0.0-compilerversion> dependency> <dependency> <groupId>org.slf4jgroupId> <artifactId>slf4j-apiartifactId> <version>1.6.3version> dependency> <dependency> <groupId>org.slf4jgroupId> <artifactId>slf4j-log4j12artifactId> <version>1.7.22version> dependency> <dependency> <groupId>redis.clientsgroupId> <artifactId>jedisartifactId> <version>2.9.0version> dependency> <dependency> <groupId>com.dyuproject.protostuffgroupId> <artifactId>protostuff-coreartifactId> <version>1.0.8version> dependency> <dependency> <groupId>com.dyuproject.protostuffgroupId> <artifactId>protostuff-runtimeartifactId> <version>1.0.8version> dependency> <dependency> <groupId>javassistgroupId> <artifactId>javassistartifactId> <version>3.11.0.GAversion> dependency> <dependency> <groupId>com.sun.elgroupId> <artifactId>el-riartifactId> <version>1.0version> dependency> <dependency> <groupId>javax.servletgroupId> <artifactId>javax.servlet-apiartifactId> <version>4.0.0version> dependency> <dependency> <groupId>org.quartz-schedulergroupId> <artifactId>quartzartifactId> <version>2.3.0version> dependency> <dependency> <groupId>org.quartz-schedulergroupId> <artifactId>quartz-jobsartifactId> <version>2.3.0version> dependency> <dependency> <groupId>javax.validationgroupId> <artifactId>validation-apiartifactId> <version>1.1.0.Finalversion> dependency> <dependency> <groupId>org.hibernategroupId> <artifactId>hibernate-validatorartifactId> <version>5.4.1.Finalversion> dependency> <dependency> <groupId>org.springframework.datagroupId> <artifactId>spring-data-redisartifactId> <version>2.0.6.RELEASEversion> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-webartifactId> <exclusions> <exclusion> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-starter-loggingartifactId> exclusion> exclusions> <version>2.0.2.RELEASEversion> dependency> <dependency> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-autoconfigureartifactId> <version>2.0.2.RELEASEversion> dependency> <dependency> <groupId>org.crazycakegroupId> <artifactId>shiro-redisartifactId> <version>2.4.2.1-RELEASEversion> dependency> dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.pluginsgroupId> <artifactId>maven-compiler-pluginartifactId> <version>3.6.1version> <configuration> <source>1.8source> <target>1.8target> configuration> plugin> <plugin> <groupId>org.springframework.bootgroupId> <artifactId>spring-boot-maven-pluginartifactId> <version>2.0.2.RELEASEversion> <configuration> <fork>truefork> <addResources>trueaddResources> configuration> plugin> plugins> <finalName>webfinalName> build> project>
spring-boot风格中spring-bean.xml中的数据源配置被省略,spring-mvc.xml中的前段控制器配置被省略,web.xml配置被省略,但是shiro的配置与spring-data-redis的配置仍需要自己配置,spring-boot推荐的方式是使用@Configuration 注解在该注解类中每个方法中使用@Bean注解的方式,大致参考:
该配置类实际相当于spring-shiro.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:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd"> <description>Shiro的配置description> <import resource="classpath:spring_shiro_redis.xml"/> <aop:aspectj-autoproxy/> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="authRealm"/> <property name="cacheManager" ref="shiroSpringCacheManager"/> bean> <bean id="authRealm" class="com.hlink.DC.shiro.CustomRealm"> <property name="credentialsMatcher" ref="passwordMatcher"/> <property name="cacheManager" ref="shiroSpringCacheManager"/> <property name="cachingEnabled" value="true"/> <property name="authenticationCachingEnabled" value="true"/> <property name="authorizationCachingEnabled" value="true"/> <property name="authenticationCacheName" value="authenticationCache"/> <property name="authorizationCacheName" value="authorizationCache"/> bean> <bean id="shiroSpringCacheManager" class="com.hlink.DC.shiro.ShiroSpringCacheManager"> <property name="redisTemplate" ref="redisTemplate">property> bean> <bean id="passwordMatcher" class="com.hlink.DC.shiro.CustomCredentialsMatcher"> <property name="cacheManager" ref="redisCacheManager"/> <property name="expire_minute" value="30"/> <property name="passwordHash" ref="passwordHash"/> bean> <bean id="passwordHash" class="com.hlink.DC.shiro.Encrypt"> <property name="hashAlgorithm" value="md5"/> <property name="hashIterations" value="5"/> bean> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="604800"/> bean> <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="sessionIdCookie"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="1800"/> bean> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('5aaC5qKm5oqA5pyvAAAAAA==')}"/> <property name="cookie" ref="rememberMeCookie"/> bean> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="globalSessionTimeout" value="1800000"/> <property name="sessionIdUrlRewritingEnabled" value="true"/> <property name="sessionIdCookie" ref="sessionIdCookie"/> <property name="sessionDAO" ref="sessionDAO"/> bean> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <property name="activeSessionsCacheName" value="activeSessionCache"/> <property name="cacheManager" ref="shiroSpringCacheManager"/> bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="successUrl" value="/index.jsp"/> <property name="unauthorizedUrl" value="/unauth.jsp"/> <property name="filterChainDefinitions"> <value> /store/admin/session = anon /store/admin/logout = anon /store/admin/login = anon /web/validationCode/** = anon /store/admin= anon /** = anon value> property> bean> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/> <property name="arguments" ref="securityManager"/> bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> <bean id="exceptionHandler" class="com.hlink.DC.shiro.GlobalExceptionHandler" /> beans>
shiro的缓存中本人使用spring-data-redis集成Redis集群来做,具体配置:
spring-shiro-redis.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:c="http://www.springframework.org/schema/c" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <description>spring-redis-cache配置文件description> <bean id="redisClusterConfiguration" class="org.springframework.data.redis.connection.RedisClusterConfiguration"> <property name="maxRedirects" value="3">property> <property name="clusterNodes"> <set> <bean class="org.springframework.data.redis.connection.RedisClusterNode"> <constructor-arg name="host" value="192.168.50.241">constructor-arg> <constructor-arg name="port" value="6000">constructor-arg> bean> <bean class="org.springframework.data.redis.connection.RedisClusterNode"> <constructor-arg name="host" value="192.168.50.241">constructor-arg> <constructor-arg name="port" value="6001">constructor-arg> bean> <bean class="org.springframework.data.redis.connection.RedisClusterNode"> <constructor-arg name="host" value="192.168.50.241">constructor-arg> <constructor-arg name="port" value="6002">constructor-arg> bean> <bean class="org.springframework.data.redis.connection.RedisClusterNode"> <constructor-arg name="host" value="192.168.50.239 ">constructor-arg> <constructor-arg name="port" value="7000">constructor-arg> bean> <bean class="org.springframework.data.redis.connection.RedisClusterNode"> <constructor-arg name="host" value="192.168.50.239">constructor-arg> <constructor-arg name="port" value="7001">constructor-arg> bean> <bean class="org.springframework.data.redis.connection.RedisClusterNode"> <constructor-arg name="host" value="192.168.50.239">constructor-arg> <constructor-arg name="port" value="7002">constructor-arg> bean> set> property> bean> <bean id="jedisPoolConfig" class="redis.clients.jedis.JedisPoolConfig"> <property name="maxIdle" value="100" /> <property name="maxTotal" value="600" /> <property name="minIdle" value="20" /> <property name="testOnBorrow" value="true" /> <property name="testOnReturn" value="true" /> <property name="testWhileIdle" value="true" /> bean> <bean id="jeidsConnectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"> <constructor-arg ref="redisClusterConfiguration" /> <constructor-arg ref="jedisPoolConfig" /> <property name="password" value="hlink">property> bean> <bean id="redisTemplate" class="org.springframework.data.redis.core.RedisTemplate"> <property name="connectionFactory" ref="jeidsConnectionFactory" /> <property name="enableTransactionSupport" value="true" /> <property name="keySerializer"> <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer">bean> property> <property name="valueSerializer"> <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" /> property> <property name="hashKeySerializer"> <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" /> property> <property name="hashValueSerializer"> <bean class="org.springframework.data.redis.serializer.JdkSerializationRedisSerializer" /> property> bean> <bean id="redisCacheManager" class="org.springframework.data.redis.cache.RedisCacheManager" factory-method="create" c:connection-factory-ref="jeidsConnectionFactory" /> beans>
shiro中的几个关键组成部分: 加密器+自定义密码比较器+全局异常拦截处理器+shiro缓存管理器,这几部分在上述的shiro集成配置中必须存在,如下:
加密器:
package com.hlink.DC.shiro; import org.apache.shiro.crypto.hash.Md5Hash; public class Encrypt { private int hashIterations = 2; private String hashAlgorithm; public int getHashIterations() { return hashIterations; } public void setHashIterations(int hashIterations) { this.hashIterations = hashIterations; } public String getHashAlgorithm() { return hashAlgorithm; } public void setHashAlgorithm(String hashAlgorithm) { this.hashAlgorithm = hashAlgorithm; } /* * 散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据, * 常见的散列算法如MD5、SHA等。一般进行散列时最好提供一个salt(盐),比如加密密码“admin”, * 产生的散列值是“21232f297a57a5a743894a0e4a801fc3”, 可以到一些md5解密网站很容易的通过散列值得到密码“admin”, * 即如果直接对密码进行散列相对来说破解更容易,此时我们可以加一些只有系统知道的干扰数据, * 如用户名和ID(即盐);这样散列的对象是“密码+用户名+ID”,这样生成的散列值相对来说更难破解。 */ // 高强度加密算法,不可逆 public String md5(String password, String salt) { return new Md5Hash(password, salt, hashIterations).toString(); } public String sha(String password, String salt) { return null; } public String hmac(String password, String salt) { return null; } public String encrypt(String password, String salt) { if (hashAlgorithm != null && hashAlgorithm.equals("md5") || hashAlgorithm.equals("MD5")) { return md5(password, salt); } /* else if(hashAlgorithm.equals("sha") || hashAlgorithm.equals("SHA")){ return null; } else if(hashAlgorithm.equals("hmac") || hashAlgorithm.equals("HMAC")){ return null; }*/ else{ return null; } } }
自定义密码比较器:
package com.hlink.DC.shiro; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authc.credential.SimpleCredentialsMatcher; public class CustomCredentialsMatcher extends SimpleCredentialsMatcher { private org.springframework.cache.CacheManager cacheManager; private int expire_minute = 30; private Encrypt passwordHash; public Encrypt getPasswordHash() { return passwordHash; } public void setPasswordHash(Encrypt passwordHash) { this.passwordHash = passwordHash; } public int getExpire_minute() { return expire_minute; } public void setExpire_minute(int expire_minute) { this.expire_minute = expire_minute; } public org.springframework.cache.CacheManager getCacheManager() { return cacheManager; } public void setCacheManager(org.springframework.cache.CacheManager cacheManager) { this.cacheManager = cacheManager; } public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { UsernamePasswordToken usertoken = (UsernamePasswordToken) token; // 注意token.getPassword()拿到的是一个char[],不能直接用toString(),它底层实现不是我们想的直接字符串,只能强转 Object tokenCredentials = passwordHash.encrypt(String.valueOf(usertoken.getPassword()), usertoken.getUsername()); Object accountCredentials = getCredentials(info); // 将密码加密与系统加密后的密码校验,内容一致就返回true,不一致就返回false System.out.println("用户输入密码:"+tokenCredentials); System.out.println("系統保存密碼: "+getCredentials(info)); return equals(tokenCredentials, accountCredentials); } }
全局异常拦截:
package com.hlink.DC.shiro; import net.sf.json.JSONObject; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.UnknownAccountException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.ResponseBody; import org.springframework.web.servlet.HandlerExceptionResolver; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @ControllerAdvice public class GlobalExceptionHandler implements HandlerExceptionResolver { @Override public ModelAndView resolveException(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) { JSONObject js = new JSONObject(); if (e instanceof UnknownAccountException) { js.put("code", "4006"); js.put("msg", "用户名不存在·"); } else if (e instanceof IncorrectCredentialsException) { js.put("code", "4006"); js.put("msg", "用户名或密码错误"); } else if (e instanceof NullPointerException) { js.put("code", "4006"); js.put("msg", "系統空指針異常"); } else if (e instanceof ClassCastException) { js.put("code", "4006"); js.put("msg", "系统类型转换异常"); } else{ js.put("code", "404"); js.put("msg", e.getMessage()); } ModelAndView mv = new ModelAndView(); mv.addObject(js); return mv; } @ExceptionHandler(value = Exception.class) @ResponseBody// 返回json数据 public JSONObject jsonErrorHandler(HttpServletRequest req, Exception e){ JSONObject js = new JSONObject(); if (e instanceof UnknownAccountException) { js.put("code", "4006"); js.put("msg", "用户名不存在·"); } else if (e instanceof IncorrectCredentialsException) { js.put("code", "4006"); js.put("msg", "用户名密码错误"); } else if (e instanceof NullPointerException) { js.put("code", "4006"); js.put("msg", "空指針異常"); } else if (e instanceof ClassCastException) { js.put("code", "4006"); js.put("msg", "类型转换异常"); } else{ js.put("code", "404"); js.put("msg", e.getMessage()); } return js; } }
shiro缓存管理器:
package com.hlink.DC.shiro; import com.hlink.DC_DB.model.User; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.Destroyable; import org.springframework.data.redis.core.BoundHashOperations; import org.springframework.data.redis.core.RedisTemplate; import java.util.Collection; import java.util.Set; /** ** 自定义cacheManage 扩张shiro里面的缓存 使用reids作缓存 * *
引入自己定义的CacheManager 关于CacheManager的配置文件在spring-redis-cache.xml中 * * * @author xxxx * @date 2018年2月3日 * @time 14:01:53 */ public class ShiroSpringCacheManager implements CacheManager, Destroyable { private String cacheKeyPrefix = "shiro_login:"; private RedisTemplate<String, Object> redisTemplate; public String getCacheKeyPrefix() { return cacheKeyPrefix; } public void setCacheKeyPrefix(String cacheKeyPrefix) { this.cacheKeyPrefix = cacheKeyPrefix; } public RedisTemplate<String, Object> getRedisTemplate() { return redisTemplate; } public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) { this.redisTemplate = redisTemplate; } @Override public <K, V> Cache<K, V> getCache(String name) throws CacheException { return new ShiroRedisCache<K, V>(cacheKeyPrefix + name); } @Override public void destroy() throws Exception { redisTemplate = null; } public class ShiroRedisCache<K, V> implements Cache<K, V> { private String cacheKey; public ShiroRedisCache(String cacheKey) { this.cacheKey = cacheKey; } @Override public V get(K key) throws CacheException { BoundHashOperations<String, K, V> hash = redisTemplate.boundHashOps(cacheKey); Object k = hashKey(key); return hash.get(k); } @SuppressWarnings("unchecked") @Override public V put(K key, V value) throws CacheException { BoundHashOperations<String, K, V> hash = redisTemplate.boundHashOps(cacheKey); Object k = hashKey(key); hash.put((K) k, value); return value; } @Override public V remove(K key) throws CacheException { BoundHashOperations<String, K, V> hash = redisTemplate.boundHashOps(cacheKey); Object k = hashKey(key); V value = hash.get(k); hash.delete(k); return value; } @Override public void clear() throws CacheException { redisTemplate.delete(cacheKey); } @Override public int size() { BoundHashOperations<String, K, V> hash = redisTemplate.boundHashOps(cacheKey); return hash.size().intValue(); } @Override public Set<K> keys() { BoundHashOperations<String, K, V> hash = redisTemplate.boundHashOps(cacheKey); return hash.keys(); } @Override public Collection<V> values() { BoundHashOperations<String, K, V> hash = redisTemplate.boundHashOps(cacheKey); return hash.values(); } protected Object hashKey(K key) { if (key instanceof PrincipalCollection) {// 此处很重要,如果key是登录凭证,那么这是访问用户的授权缓存;将登录凭证转为user对象,返回user的id属性做为hash // key,否则会以user对象做为hash key,这样就不好清除指定用户的缓存了 PrincipalCollection pc = (PrincipalCollection) key; User user = (User) pc.getPrimaryPrincipal(); return user.getId(); } return key; } } }
最后在贴一下我的启动类:
package com.hlink.DC; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.ImportResource; import org.springframework.transaction.annotation.EnableTransactionManagement; @EnableAutoConfiguration @EnableTransactionManagement @ComponentScan @ImportResource(locations = {"classpath:applicationContext.xml", "classpath:spring_shiro_redis.xml", "classpath:spring-shiro.xml", "classpath:spring-dubbo.xml"}) public class DCApplication { public static void main(String[] args) { SpringApplication.run(DCApplication.class, args); } }
spring-boot对市场上常见的框架并不是完全集成的,例如dubbo等,但它的配置加载却具有相当的灵活性,开发者仍可以在开始接触的过渡期间使用自己的配置文件(applicationContext.xml,spring-shiro.xml.spring-dubbo.xml等)只需要在启动类中引入加载即可使用,但切记springboot已经完美的兼容了springmvc,所以不要再多余配置spring-mvc.xml中的所有配置
后言:以上内容均属个人感悟,如有错误欢迎指正,谢谢!!