diff --git a/README.md b/README.md index 285b1f8..905c149 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,12 @@ * 是博客 springboot集成shiro 实现权限控制 地址:http://blog.csdn.net/u012373815/article/details/57532292 +##springboot-shior2 + +是使用shior 框架调取用户权限服务,进行登录权限验证的例子,其中的用户权限服务没有写,都是用TODO 标示出来了,使用时可以根据各自的用户权限服务进行编码替换 + +springboot-shiro2 也是和dubbo 的结合例子是 消费者的示例。 + ## springboot-swagger-ui * 博客 spring boot +Swagger-ui 自动生成API文档 地址: https://blog.csdn.net/u012373815/article/details/82685962 @@ -72,5 +78,11 @@ ## Springboot多数据源切换 * springboot 多个数据源的配置, 一个springboot 项目操作多个数据库的数据:https://abelyang.blog.csdn.net/article/details/89296341 +##springboot-dubbo + +该项目是Springboot 和 dubbo 结合的例子,是provider 的示例,提供服务。简单的写了一些用户和权限的接口没有写的很完整,主要是为了提现dubbo 服务 +Springboot-shiro2 也是和dubbo 的结合例子是 消费者的示例。 + + ##未完待续。。。 diff --git a/springboot-dubbo/README.md b/springboot-dubbo/README.md new file mode 100644 index 0000000..16441b5 --- /dev/null +++ b/springboot-dubbo/README.md @@ -0,0 +1,10 @@ +##springboot-dubbo + +该项目是Springboot 和 dubbo 结合的例子,是provider 的示例,提供服务。简单的写了一些用户和权限的接口没有写的很完整,主要是为了提现dubbo 服务 +Springboot-shiro2 也是和dubbo 的结合例子是 消费者的示例。 + +### abel-user-api +该模块是让消费者 引用的 api 模块,主要用来定义接口和数据传输实体 + +### abel-user-provider +该模块是真正的微服务提供者 diff --git a/springboot-shiro2/README.md b/springboot-shiro2/README.md new file mode 100644 index 0000000..8fe9437 --- /dev/null +++ b/springboot-shiro2/README.md @@ -0,0 +1,6 @@ +##springboot-shior2 + +是使用shior 框架调取用户权限服务,进行登录权限验证的例子,其中的用户权限服务没有写,都是用TODO 标示出来了,使用时可以根据各自的用户权限服务进行编码替换 + +springboot-shiro2 也是和dubbo 的结合例子是 消费者的示例。 +引入了 abel-user-api 模块 \ No newline at end of file diff --git a/springboot-shiro2/pom.xml b/springboot-shiro2/pom.xml new file mode 100644 index 0000000..8a3a9eb --- /dev/null +++ b/springboot-shiro2/pom.xml @@ -0,0 +1,142 @@ + + + 4.0.0 + + cn.abel + abel-parent + 1.0.0-SNAPSHOT + + + cn.abel + springboot-shior2 + 1.0.0 + jar + Spring Boot + + + + cn.abel + abel-util + 1.0.0-SNAPSHOT + + + + cn.abel + abel-user-api + 1.0.0-SNAPSHOT + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-quartz + + + org.springframework.boot + spring-boot-starter-data-redis + + + org.springframework.boot + spring-boot-starter-freemarker + + + org.springframework.boot + spring-boot-starter-test + test + + + + org.apache.commons + commons-pool2 + + + org.apache.commons + commons-collections4 + + + + com.alibaba + fastjson + + + + com.alibaba + dubbo + + + org.slf4j + log4j-over-slf4j + + + org.apache.zookeeper + zookeeper + + + + + org.apache.shiro + shiro-spring + + + org.apache.shiro + shiro-ehcache + + + org.apache.shiro + shiro-quartz + + + org.apache.shiro + shiro-ehcache + + + net.mingsoft + shiro-freemarker-tags + + + com.fasterxml.jackson.core + jackson-databind + + + com.fasterxml.jackson.core + jackson-annotations + + + 2.9.9.1 + + + + com.fasterxml.jackson.core + jackson-core + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + + + + \ No newline at end of file diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/ShiroRestApplication.java b/springboot-shiro2/src/main/java/cn/abel/rest/ShiroRestApplication.java new file mode 100644 index 0000000..0f1102c --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/ShiroRestApplication.java @@ -0,0 +1,19 @@ +package cn.abel.rest; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; +import org.springframework.context.annotation.ImportResource; + +/** + * 无数据库运行 + * + */ +@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class}) +@ImportResource("classpath*:META-INF/spring/*.xml") +public class ShiroRestApplication { + + public static void main(String[] args) { + SpringApplication.run(ShiroRestApplication.class, args); + } +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/config/RedisConfig.java b/springboot-shiro2/src/main/java/cn/abel/rest/config/RedisConfig.java new file mode 100644 index 0000000..821e775 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/config/RedisConfig.java @@ -0,0 +1,199 @@ +package cn.abel.rest.config; + +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; + +import cn.abel.rest.constants.Constants; +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.PropertyAccessor; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Primary; +import org.springframework.data.redis.cache.RedisCacheConfiguration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.cache.RedisCacheWriter; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisPassword; +import org.springframework.data.redis.connection.RedisStandaloneConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration; +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; +import org.springframework.data.redis.connection.lettuce.LettucePoolingClientConfiguration; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; +import org.springframework.data.redis.serializer.RedisSerializationContext; +import org.springframework.data.redis.serializer.RedisSerializer; +import org.springframework.data.redis.serializer.StringRedisSerializer; + +/** + * redis缓存配置 + * + */ +@Configuration +@EnableCaching +public class RedisConfig extends CachingConfigurerSupport { + + /** logger */ + private static final Logger logger = LoggerFactory.getLogger(RedisConfig.class); + /** + * serializer + */ + private static final StringRedisSerializer STRING_SERIALIZER = new StringRedisSerializer(); + + @Value("${spring.redis.database}") + private Integer database; + @Value("${spring.redis.host}") + private String host; + @Value("${spring.redis.port}") + private Integer port; + @Value("${spring.redis.password}") + private String password; + @Value("${spring.redis.lettuce.pool.max-active}") + private Integer maxActive; + @Value("${spring.redis.lettuce.pool.max-wait}") + private Integer maxWait; + @Value("${spring.redis.lettuce.pool.max-idle}") + private Integer maxIdle; + @Value("${spring.redis.lettuce.pool.min-idle}") + private Integer minIdle; + @Value("${spring.redis.lettuce.shutdown-timeout}") + private Integer timeout; + + /** + * 在使用@Cacheable时,如果不指定key,则使用这个默认的key生成器生成的key + * + * @return + */ + @Override + @Bean + public KeyGenerator keyGenerator() { + return (target, method, params) -> { + StringBuilder sb = new StringBuilder(); + sb.append(target.getClass().getName()); + sb.append(method.getName()); + for (Object obj : params) { + sb.append(obj.toString()); + } + return sb.toString(); + }; + } + + /** + * 声明缓存管理器 + */ + @Override + @Bean + public CacheManager cacheManager() { + //1. entryTtl: 定义默认的cache time-to-live. + //2. disableCachingNullValues: 禁止缓存Null对象. 视需求而定. + //3. computePrefixWith: 此处定义了cache key的前缀, 避免公司不同项目之间的key名称冲突. + //4. serializeKeysWith, serializeValuesWith: 定义key和value的序列化协议, 同时的hash key和hash value也被定义. + + // 缓存配置 + RedisCacheConfiguration cacheConfiguration = + RedisCacheConfiguration.defaultCacheConfig().disableCachingNullValues().computePrefixWith(cacheName -> Constants.REDIS_KEY_PRE.concat(":").concat(cacheName).concat(":")).entryTtl(Duration.ofDays(Constants.REDIS_TIMEOUT)); + + Map redisCacheConfigurationMap = new HashMap<>(2); + //redisCacheConfigurationMap.put(Constants.CACHE_NAME_PERMISSION, permissionCacheConfiguration); + + //初始化一个RedisCacheWriter + RedisCacheWriter redisCacheWriter = RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory()); + + RedisSerializationContext.SerializationPair valuePair = + RedisSerializationContext.SerializationPair.fromSerializer(STRING_SERIALIZER); + //RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer(new ObjectMapper())); + RedisCacheConfiguration defaultCacheConfig = + RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(valuePair); + //设置默认过期时间是1天 + defaultCacheConfig.entryTtl(Duration.ofDays(Constants.REDIS_TIMEOUT)); + + //初始化RedisCacheManager + RedisCacheManager cacheManager = new RedisCacheManager(redisCacheWriter, defaultCacheConfig, + redisCacheConfigurationMap); + cacheManager.afterPropertiesSet(); + logger.info("RedisCacheManager config success"); + + return cacheManager; + } + + @Bean(name = "redisTemplate") + @Primary + public RedisTemplate redisTemplate() { + return getTemplate(redisConnectionFactory()); + } + + @Bean(name = "stringRedisTemplate") + public StringRedisTemplate stringRedisTemplate() { + return new StringRedisTemplate(redisConnectionFactory()); + } + + private RedisConnectionFactory redisConnectionFactory() { + return connectionFactory(maxActive, maxIdle, minIdle, maxWait, host, password, timeout, port, database); + } + + private RedisConnectionFactory connectionFactory(Integer maxActive, + Integer maxIdle, + Integer minIdle, + Integer maxWait, + String host, + String password, + Integer timeout, + Integer port, + Integer database) { + RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration(); + redisStandaloneConfiguration.setHostName(host); + redisStandaloneConfiguration.setPort(port); + redisStandaloneConfiguration.setDatabase(database); + redisStandaloneConfiguration.setPassword(RedisPassword.of(password)); + + GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig(); + poolConfig.setMaxTotal(maxActive); + poolConfig.setMaxIdle(maxIdle); + poolConfig.setMinIdle(minIdle); + poolConfig.setMaxWaitMillis(maxWait); + LettuceClientConfiguration lettucePoolingConfig = LettucePoolingClientConfiguration.builder() + //.commandTimeout(Duration.ofSeconds(15)) + .poolConfig(poolConfig).shutdownTimeout(Duration.ofMillis(timeout)).build(); + LettuceConnectionFactory connectionFactory = new LettuceConnectionFactory(redisStandaloneConfiguration, + lettucePoolingConfig); + connectionFactory.afterPropertiesSet(); + + return connectionFactory; + } + + private RedisTemplate getTemplate(RedisConnectionFactory factory) { + RedisTemplate template = new RedisTemplate(); + template.setConnectionFactory(factory); + template.setKeySerializer(STRING_SERIALIZER); + template.setValueSerializer(jackson2JsonRedisSerializer()); + //不能用stringSerializer,有地方使用ShiroUser做key,不能转换 + //template.setHashKeySerializer(STRING_SERIALIZER); + //template.setHashKeySerializer(jackson2JsonRedisSerializer()); + + template.afterPropertiesSet(); + + return template; + } + + private RedisSerializer jackson2JsonRedisSerializer() { + Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = + new Jackson2JsonRedisSerializer<>(Object.class); + ObjectMapper objectMapper = new ObjectMapper(); + jackson2JsonRedisSerializer.setObjectMapper(objectMapper); + objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); + objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); + jackson2JsonRedisSerializer.setObjectMapper(objectMapper); + return jackson2JsonRedisSerializer; + } + +} \ No newline at end of file diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/constants/Constants.java b/springboot-shiro2/src/main/java/cn/abel/rest/constants/Constants.java new file mode 100644 index 0000000..07702de --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/constants/Constants.java @@ -0,0 +1,19 @@ +package cn.abel.rest.constants; + + +public interface Constants { + + /** + * 第三方登录的token。 + */ + String THIRD_PARTY_ACCESS_TOKEN_NAME = "accessToken"; + + /** 用户注册和绑定时的ip */ + String CLIENT_IP = "127.0.0.1"; + /** redis缓存 key前缀 */ + String REDIS_KEY_PRE = "abel-user"; + /** 超时时间(天) */ + long REDIS_TIMEOUT = 10L; + + +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/controller/LoginController.java b/springboot-shiro2/src/main/java/cn/abel/rest/controller/LoginController.java new file mode 100644 index 0000000..33d6b5f --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/controller/LoginController.java @@ -0,0 +1,82 @@ +package cn.abel.rest.controller; +import cn.abel.response.ResponseEntity; +import cn.abel.rest.shiro.ShiroUser; +import cn.abel.user.models.User; +import cn.abel.user.service.PermissionService; +import cn.abel.user.service.RoleService; +import cn.abel.user.service.UserService; +import com.alibaba.dubbo.config.annotation.Reference; +import com.google.common.collect.Sets; +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.subject.Subject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + +/** + * @author yyb + * @time 2020/3/6 + */ +@RestController +@CrossOrigin +@RequestMapping("/") +public class LoginController { + + private static final Logger logger = LoggerFactory.getLogger(LoginController.class); + + @Reference + private UserService userService; + + + /** + * @return + */ + @RequestMapping(value = "/login", method = RequestMethod.POST) + public ResponseEntity login() { + //TODO:登录日志。 + ShiroUser user = (ShiroUser) SecurityUtils.getSubject().getPrincipal(); + user.setToken(SecurityUtils.getSubject().getSession().getId().toString()); + return ResponseEntity.ok(user); + } + + /** + * 获取当前登录人的登录信息,包括拥有的角色、权限等。 + * + * @return + */ + @GetMapping("/logininfo") + public ResponseEntity loginInfo() { + ShiroUser shiroUser = (ShiroUser) SecurityUtils.getSubject().getPrincipal(); + + Map map = new HashMap<>(); + Set permissions = Sets.newHashSet(); + //TODO 获取当前用户所有的权限和角色 + User user = userService.getById(shiroUser.getUserId().intValue()); + map.put("roleList", user.getRoles()); + map.put("permissionList", permissions); + map.put("userId", shiroUser.getUserId()); + map.put("username", shiroUser.getLoginName()); + + return ResponseEntity.ok(map); + } + + + /** + * 注销 + * @return + */ + @PostMapping("/logout") + public ResponseEntity logout() { + Subject subject = SecurityUtils.getSubject(); + subject.logout(); + return ResponseEntity.ok(); + } + + +} + + diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/exception/DefaultErrorController.java b/springboot-shiro2/src/main/java/cn/abel/rest/exception/DefaultErrorController.java new file mode 100644 index 0000000..c4a5f15 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/exception/DefaultErrorController.java @@ -0,0 +1,59 @@ +package cn.abel.rest.exception; + +import java.util.HashMap; +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +import org.springframework.boot.autoconfigure.web.ServerProperties; +import org.springframework.boot.autoconfigure.web.servlet.error.BasicErrorController; +import org.springframework.boot.web.servlet.error.DefaultErrorAttributes; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.servlet.ModelAndView; + +/** + * 404 覆盖默认error处理方法(不设置则在根目录查找error页面) + * + */ +@Controller +public class DefaultErrorController extends BasicErrorController { + + public DefaultErrorController(ServerProperties serverProperties) { + super(new DefaultErrorAttributes(), serverProperties.getError()); + } + + /** + * 覆盖默认的Json响应 + */ + @Override + public ResponseEntity> error(HttpServletRequest request) { + Map body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); + HttpStatus status = getStatus(request); + + //输出自定义的Json格式 + Map map = new HashMap<>(2); + map.put("status", false); + map.put("msg", body.get("message")); + + return new ResponseEntity<>(map, status); + } + + /** + * 覆盖默认的HTML响应 + */ + @Override + public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { + //请求的状态 + HttpStatus status = getStatus(request); + response.setStatus(getStatus(request).value()); + + Map model = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.TEXT_HTML)); + ModelAndView modelAndView = resolveErrorView(request, response, status, model); + //指定自定义的视图 + return (modelAndView == null ? new ModelAndView("error/error", model) : modelAndView); + } +} \ No newline at end of file diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/exception/DefaultExceptionHandler.java b/springboot-shiro2/src/main/java/cn/abel/rest/exception/DefaultExceptionHandler.java new file mode 100644 index 0000000..c9f645f --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/exception/DefaultExceptionHandler.java @@ -0,0 +1,142 @@ +package cn.abel.rest.exception; + +import cn.abel.exception.ServiceException; +import cn.abel.response.ResponseEntity; +import org.apache.shiro.authz.UnauthorizedException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.ui.Model; +import org.springframework.web.bind.MethodArgumentNotValidException; +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.bind.annotation.ResponseStatus; +import org.springframework.web.context.request.NativeWebRequest; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import javax.validation.ConstraintViolationException; + +/** + * 统一异常处理 + * + */ +@ControllerAdvice +public class DefaultExceptionHandler { + + /** + * logger + */ + private static final Logger logger = LoggerFactory.getLogger(DefaultExceptionHandler.class); + + private static final String ERROR_500 = "error/500"; + private static final String ERROR_403 = "error/403"; + private static final int ARG_ERROR_CODE = 400; + + + /** + * 参数验证异常。 + * + * @param ex + * @return + */ + @ResponseBody + @ResponseStatus(HttpStatus.OK) + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleMethodArgumentNotValidException( + HttpServletRequest request, + HttpServletResponse response, + MethodArgumentNotValidException ex) { + String errMsg = null; + if (ex.getBindingResult() != null && ex.getBindingResult().hasErrors()) { + errMsg = ex.getBindingResult().getFieldErrors().get(0).getDefaultMessage(); + } + return ResponseEntity.error(ARG_ERROR_CODE, errMsg); + } + + + /** + * 参数验证异常。 + * + * @param ex + * @return + */ + @ResponseBody + @ResponseStatus(HttpStatus.OK) + @ExceptionHandler(ConstraintViolationException.class) + public ResponseEntity handleConstraintViolationException( + HttpServletRequest request, + HttpServletResponse response, + ConstraintViolationException ex) { + String errMsg = ((ConstraintViolationException) ex).getConstraintViolations() + .iterator().next().getMessage(); + return ResponseEntity.error(ARG_ERROR_CODE, errMsg); + } + + + /** + * ServiceException异常 + * + * @param exception + * @return + */ + @ExceptionHandler({ServiceException.class}) + @ResponseStatus(HttpStatus.OK) + public String processServiceException(Exception exception, Model model) { + model.addAttribute("exception", exception.getMessage()); + logger.error("rpc接口调用异常。{}", exception.getMessage()); + return ERROR_500; + } + + /** + * 没有权限 异常 + *

+ * 后续根据不同的需求定制即可 + * 应用到所有@RequestMapping注解的方法,在其抛出UnauthorizedException异常时执行 + * + * @param request + * @param e + * @return + */ + @ExceptionHandler({UnauthorizedException.class}) + @ResponseStatus(HttpStatus.UNAUTHORIZED) + public String processUnauthorizedException(NativeWebRequest request, Model model, UnauthorizedException e) { + model.addAttribute("exception", e.getMessage()); + logger.error("权限异常。{}", e.getMessage()); + return ERROR_403; + } + + /** + * 运行时异常 + * + * @param exception + * @return + */ + @ExceptionHandler({RuntimeException.class}) + @ResponseStatus(HttpStatus.OK) + public String processException(RuntimeException exception, Model model) { + model.addAttribute("exception", exception.getMessage()); + logger.error("程序异常", exception); + return ERROR_500; + } + + /** + * Exception异常 + * + * @param exception + * @return + */ + @ExceptionHandler({Exception.class}) + @ResponseStatus(HttpStatus.OK) + public String processException(Exception exception, Model model) { + model.addAttribute("exception", exception.getMessage()); + logger.error("程序异常", exception); + return ERROR_500; + //logger.info("自定义异常处理-Exception"); + //ModelAndView m = new ModelAndView(); + //m.addObject("exception", exception.getMessage()); + //m.setViewName("error/500"); + //return m; + } +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/freemarker/CustomFreeMarkerView.java b/springboot-shiro2/src/main/java/cn/abel/rest/freemarker/CustomFreeMarkerView.java new file mode 100644 index 0000000..b0d9929 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/freemarker/CustomFreeMarkerView.java @@ -0,0 +1,29 @@ +package cn.abel.rest.freemarker; + +import java.util.Map; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.web.servlet.view.freemarker.FreeMarkerView; + +/** + * freemarker全局变量 + * + * @date 2018/08/12 17:06 + */ +public class CustomFreeMarkerView extends FreeMarkerView { + + private static final String CONTEXT_PATH = "base"; + + @Override + protected void exposeHelpers(Map model, HttpServletRequest request) throws Exception { + String scheme = request.getScheme(); + String serverName = request.getServerName(); + int port = request.getServerPort(); + String path = request.getContextPath(); + String basePath = scheme + "://" + serverName + ":" + port + path; + model.put(CONTEXT_PATH, basePath); + + super.exposeHelpers(model, request); + } +} \ No newline at end of file diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/freemarker/FreeMarkerConfig.java b/springboot-shiro2/src/main/java/cn/abel/rest/freemarker/FreeMarkerConfig.java new file mode 100644 index 0000000..c290e63 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/freemarker/FreeMarkerConfig.java @@ -0,0 +1,38 @@ +package cn.abel.rest.freemarker; + +import javax.annotation.PostConstruct; + +import com.jagregory.shiro.freemarker.ShiroTags; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.view.InternalResourceViewResolver; +import org.springframework.web.servlet.view.freemarker.FreeMarkerViewResolver; + +/** + * freemarker配置 + * + * @date 2018/08/13 11:43 + */ +@Configuration +public class FreeMarkerConfig { + + @Autowired + freemarker.template.Configuration config; + @Autowired + FreeMarkerViewResolver resolver; + @Autowired + InternalResourceViewResolver springResolver; + + @PostConstruct + public void setSharedVariable() { + config.setNumberFormat("0.##"); + config.setDateFormat("yyyy/MM/dd"); + config.setDateTimeFormat("yyyy-MM-dd HH:mm:ss"); + + //自定义视图变量和方法 + resolver.setViewClass(CustomFreeMarkerView.class); + + //shiro标签 + config.setSharedVariable("shiro", new ShiroTags()); + } +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/HttpHeaderSessionManager.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/HttpHeaderSessionManager.java new file mode 100644 index 0000000..0e680aa --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/HttpHeaderSessionManager.java @@ -0,0 +1,27 @@ +package cn.abel.rest.shiro; + +import org.apache.shiro.web.servlet.ShiroHttpServletRequest; +import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; +import org.apache.shiro.web.util.WebUtils; +import org.springframework.util.StringUtils; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import java.io.Serializable; + +public class HttpHeaderSessionManager extends DefaultWebSessionManager { + public static final String LOGIN_TOKEN_KEY = "sid"; + private static final String REFERENCED_SESSION_ID_SOURCE = "header"; + + @Override + protected Serializable getSessionId(ServletRequest request, ServletResponse response) { + String id = WebUtils.toHttp(request).getHeader(LOGIN_TOKEN_KEY); + if (!StringUtils.isEmpty(id)) { + request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, REFERENCED_SESSION_ID_SOURCE); + request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); + request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); + return id; + } + return null; + } +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/RedisSessionDao.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/RedisSessionDao.java new file mode 100644 index 0000000..742e36f --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/RedisSessionDao.java @@ -0,0 +1,99 @@ +package cn.abel.rest.shiro; + +import java.io.Serializable; + +import javax.servlet.http.HttpServletRequest; + +import cn.abel.rest.utils.ServletKit; +import org.apache.shiro.cache.Cache; +import org.apache.shiro.session.Session; +import org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 重写SessionDAO,实现session的CRUD功能 + * + * @date 2019/02/22 14:06 + */ +public class RedisSessionDao extends EnterpriseCacheSessionDAO { + + /** logger */ + private static final Logger logger = LoggerFactory.getLogger(RedisSessionDao.class); + + private Cache cache() { + Cache cache = getCacheManager().getCache(this.getClass().getName()); + return cache; + } + + /** + * 创建session,保存到数据库 + * + * @param session + * @return + */ + @Override + protected Serializable doCreate(Session session) { + Serializable sessionId = super.doCreate(session); + cache().put(sessionId.toString(), session); + return sessionId; + } + + /** + * 获取session + * + * @param sessionId + * @return + */ + @Override + protected Session doReadSession(Serializable sessionId) { + Session session = null; + HttpServletRequest request = ServletKit.getRequest(); + if (request != null) { + String uri = request.getServletPath(); + if (ServletKit.isStaticFile(uri)) { + return null; + } + session = (Session) request.getAttribute("session_" + sessionId); + } + if (session == null) { + session = super.doReadSession(sessionId); + } + if (session == null) { + session = (Session) cache().get(sessionId.toString()); + } + return session; + } + + /** + * 更新session的最后一次访问时间 + * + * @param session + */ + @Override + protected void doUpdate(Session session) { + HttpServletRequest request = ServletKit.getRequest(); + if (request != null) { + String uri = request.getServletPath(); + if (ServletKit.isStaticFile(uri)) { + return; + } + } + + super.doUpdate(session); + cache().put(session.getId().toString(), session); + + logger.debug("{}", session.getAttribute("shiroUserId")); + } + + /** + * 删除session + * + * @param session + */ + @Override + protected void doDelete(Session session) { + super.doDelete(session); + cache().remove(session.getId().toString()); + } +} \ No newline at end of file diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroConfig.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroConfig.java new file mode 100644 index 0000000..690d502 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroConfig.java @@ -0,0 +1,299 @@ +package cn.abel.rest.shiro; + +import java.util.LinkedHashMap; +import java.util.Map; + +import javax.servlet.Filter; + +import cn.abel.rest.shiro.ext.QuartzSessionValidationScheduler; +import cn.abel.rest.shiro.credentials.RetryLimitHashedCredentialsMatcher; +import cn.abel.rest.shiro.filter.ShiroFormAuthenticationFilter; +import cn.abel.rest.shiro.filter.ShiroLogoutFilter; +import org.apache.shiro.authc.credential.CredentialsMatcher; +import org.apache.shiro.cache.CacheManager; +import org.apache.shiro.codec.Base64; +import org.apache.shiro.mgt.RememberMeManager; +import org.apache.shiro.mgt.SecurityManager; +import org.apache.shiro.realm.Realm; +import org.apache.shiro.session.mgt.SessionValidationScheduler; +import org.apache.shiro.session.mgt.eis.SessionDAO; +import org.apache.shiro.spring.LifecycleBeanPostProcessor; +import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; +import org.apache.shiro.spring.web.ShiroFilterFactoryBean; +import org.apache.shiro.web.mgt.CookieRememberMeManager; +import org.apache.shiro.web.mgt.DefaultWebSecurityManager; +import org.apache.shiro.web.servlet.Cookie; +import org.apache.shiro.web.servlet.SimpleCookie; +import org.apache.shiro.web.session.mgt.DefaultWebSessionManager; +import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; +import org.springframework.beans.factory.config.MethodInvokingFactoryBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.DependsOn; + + +/** + * @date 2018/08/07 23:43 + */ +@Configuration +public class ShiroConfig { + public final static String SHIRO_REALM_NAME = "shiroRealm"; + + /** + * shiro的Web过滤器 + * + * @param securityManager + * @return + */ + @Bean(name = "shiroFilter") + public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { + ShiroFilterFactoryBean bean = new ShiroFilterFactoryBean(); + bean.setSecurityManager(securityManager); + + //配置鉴权与登出的拦截器。 + Map filterMap = new LinkedHashMap<>(); + filterMap.put("authc", formAuthenticationFilter()); + filterMap.put("logout", new ShiroLogoutFilter()); + bean.setFilters(filterMap); + + //配置各路径需要使用的拦截器。 + //anon表示anonymous。 + Map filterChainDefinitionMap = new LinkedHashMap<>(); + filterChainDefinitionMap.put("/logout", "logout"); + filterChainDefinitionMap.put("/css/**", "anon"); + filterChainDefinitionMap.put("/js/**", "anon"); + filterChainDefinitionMap.put("/img/**", "anon"); + filterChainDefinitionMap.put("/font/**", "anon"); + filterChainDefinitionMap.put("/fonts/**", "anon"); + filterChainDefinitionMap.put("/images/**", "anon"); + + filterChainDefinitionMap.put("/**", "authc"); + bean.setFilterChainDefinitionMap(filterChainDefinitionMap); + return bean; + } + + /** + * 缓存管理器 使用Ehcache实现 + * + * @return + */ + @Bean(name = "shiroRedisCacheManager") + //@ConditionalOnClass(name = {"org.apache.shiro.cache.ehcache.EhCacheManager"}) + @ConditionalOnMissingBean(name = "shiroRedisCacheManager") + public CacheManager shiroRedisCacheManager() { + //EhCacheManager cacheManager = new EhCacheManager(); + //cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml"); + //return cacheManager; + ShiroRedisCacheManager cacheManager = new ShiroRedisCacheManager(); + return cacheManager; + } + + /** + * realm + * + * @return + */ + @Bean + public Realm realm() { + ShiroRealm shiroRealm = new ShiroRealm(); + shiroRealm.setName(SHIRO_REALM_NAME); + shiroRealm.setCredentialsMatcher(credentialsMatcher()); + shiroRealm.setCachingEnabled(true); + + //不设置账号缓存,每次登录都实时调用用户服务的登录接口。 + shiroRealm.setAuthenticationCachingEnabled(false); + + //不设置角色权限缓存,每次登录都从权限服务实时拉取。 + shiroRealm.setAuthorizationCachingEnabled(false); + + return shiroRealm; + } + + /** + * 会话验证调度器 + * + * @return + */ + @Bean(name = "sessionValidationScheduler") + @ConditionalOnClass(name = {"org.quartz.Scheduler"}) + @ConditionalOnMissingBean(SessionValidationScheduler.class) + public SessionValidationScheduler sessionValidationScheduler(DefaultWebSessionManager sessionManager) { + QuartzSessionValidationScheduler scheduler = new QuartzSessionValidationScheduler(sessionManager); + //scheduler.setSessionValidationInterval(1800000); + //scheduler.setSessionManager(sessionManager()); + sessionManager.setSessionValidationSchedulerEnabled(true); + sessionManager.setSessionValidationInterval(1800000); + sessionManager.setSessionValidationScheduler(scheduler); + return scheduler; + } + + /** + * 会话DAO + * + * @return + */ + @Bean + public SessionDAO sessionDAO() { + RedisSessionDao redisSessionDao = new RedisSessionDao(); + redisSessionDao.setActiveSessionsCacheName("shiro-activeSessionCache"); + redisSessionDao.setSessionIdGenerator(new UuidSessionIdGenerator()); + return redisSessionDao; + } + + /** + * 会话Cookie模板 + * + * @return + */ + @Bean + public Cookie sessionIdCookie() { + SimpleCookie cookie = new SimpleCookie("sid"); + cookie.setHttpOnly(true); + cookie.setMaxAge(-1); + return cookie; + } + + /** + * rememberMe Cookie + * + * @return + */ + @Bean + public Cookie rememberMeCookie() { + SimpleCookie cookie = new SimpleCookie("rememberMe"); + cookie.setHttpOnly(true); + //30天 + cookie.setMaxAge(2592000); + return cookie; + } + + /** + * 会话管理器 + * + * @return + */ + @Bean + public DefaultWebSessionManager sessionManager() { + //SessionManager sessionManager = new SessionManager(); + HttpHeaderSessionManager sessionManager = new HttpHeaderSessionManager(); + sessionManager.setGlobalSessionTimeout(43200000); + sessionManager.setDeleteInvalidSessions(true); + //sessionManager.setSessionValidationSchedulerEnabled(true); + //sessionManager.setSessionValidationScheduler(sessionValidationScheduler(sessionManager)); + //sessionManager.setSessionValidationScheduler(sessionValidationScheduler()); + sessionManager.setSessionDAO(sessionDAO()); + sessionManager.setSessionIdCookieEnabled(false); +// sessionManager.setSessionIdCookie(sessionIdCookie()); + //去掉URL中的JSESSIONID + sessionManager.setSessionIdUrlRewritingEnabled(false); + return sessionManager; + } + + /** + * 安全管理器 + * + * @return + */ + @Bean + @DependsOn("redisTemplate") + public SecurityManager securityManager() { + DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); + securityManager.setRealm(realm()); + securityManager.setCacheManager(shiroRedisCacheManager()); + securityManager.setSessionManager(sessionManager()); + securityManager.setRememberMeManager(rememberMeManager()); + return securityManager; + } + + /** + * rememberMe管理器 + * + * @return + */ + @Bean + public RememberMeManager rememberMeManager() { + CookieRememberMeManager manager = new CookieRememberMeManager(); + //rememberMe cookie加密的密钥 建议每个项目都不一样 默认AES算法 密钥长度(128 256 512 位) + manager.setCipherKey(Base64.decode("4AvVhmFLUs0KTA3Kprsdag==")); + manager.setCookie(rememberMeCookie()); + return manager; + } + + /** + * 凭证匹配器 + * + * @return + */ + @Bean(name = "credentialsMatcher") + public CredentialsMatcher credentialsMatcher() { + //明文密码 + //CustomCredentialsMatcher matcher = new CustomCredentialsMatcher(); + //HashedCredentialsMatcher matcher = new HashedCredentialsMatcher(); + RetryLimitHashedCredentialsMatcher matcher = new RetryLimitHashedCredentialsMatcher(shiroRedisCacheManager()); + //matcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法; + //matcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); + matcher.setStoredCredentialsHexEncoded(true); + return matcher; + } + + @Bean + public MethodInvokingFactoryBean methodInvokingFactoryBean() { + MethodInvokingFactoryBean factoryBean = new MethodInvokingFactoryBean(); + factoryBean.setStaticMethod("org.apache.shiro.SecurityUtils.setSecurityManager"); + factoryBean.setArguments(securityManager()); + return factoryBean; + } + + /** + * Shiro生命周期处理器 + * + * @return + */ + @Bean + public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { + return new LifecycleBeanPostProcessor(); + } + + public ShiroFormAuthenticationFilter formAuthenticationFilter() { + ShiroFormAuthenticationFilter filter = new ShiroFormAuthenticationFilter(); + filter.setUsernameParam("username"); + filter.setPasswordParam("password"); + filter.setRememberMeParam("rememberMe"); + filter.setLoginUrl("/login"); + filter.setSuccessUrl("/index"); + return filter; + } + + /** + * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类, + * 并在必要时进行安全逻辑验证 * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor) + * 即可实现此功能 + * + * @return + */ + @Bean + @DependsOn({"lifecycleBeanPostProcessor"}) + public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() { + DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); + advisorAutoProxyCreator.setProxyTargetClass(true); + return advisorAutoProxyCreator; + } + + /** + * 开启shiro aop注解支持. + * 使用代理方式;所以需要开启代码支持; + * 需要aspectj支持 + * + * @param securityManager + * @return + */ + @Bean + public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) { + AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = + new AuthorizationAttributeSourceAdvisor(); + authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); + return authorizationAttributeSourceAdvisor; + } +} \ No newline at end of file diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroProperty.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroProperty.java new file mode 100644 index 0000000..fac4c38 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroProperty.java @@ -0,0 +1,55 @@ +package cn.abel.rest.shiro; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Configuration; + +/** + * @date 2019/02/26 14:39 + */ +@Configuration +public class ShiroProperty { + + @Value("${shiro.retryExpireTimeRedis}") + private int shiroRetryExpireTimeRedis; + + @Value("${shiro.authorizationExpireTimeRedis}") + private int shiroAuthorizationExpireTimeRedis; + + @Value("${shiro.retryMax}") + private int shiroRetryMax; + + @Value("${shiro.sessionExpireTimeRedis}") + private Integer shiroSessionExpireTimeRedis; + + public int getShiroRetryExpireTimeRedis() { + return shiroRetryExpireTimeRedis; + } + + public void setShiroRetryExpireTimeRedis(int shiroRetryExpireTimeRedis) { + this.shiroRetryExpireTimeRedis = shiroRetryExpireTimeRedis; + } + + public int getShiroAuthorizationExpireTimeRedis() { + return shiroAuthorizationExpireTimeRedis; + } + + public void setShiroAuthorizationExpireTimeRedis(int shiroAuthorizationExpireTimeRedis) { + this.shiroAuthorizationExpireTimeRedis = shiroAuthorizationExpireTimeRedis; + } + + public int getShiroRetryMax() { + return shiroRetryMax; + } + + public void setShiroRetryMax(int shiroRetryMax) { + this.shiroRetryMax = shiroRetryMax; + } + + public Integer getShiroSessionExpireTimeRedis() { + return shiroSessionExpireTimeRedis; + } + + public void setShiroSessionExpireTimeRedis(Integer shiroSessionExpireTimeRedis) { + this.shiroSessionExpireTimeRedis = shiroSessionExpireTimeRedis; + } +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroRealm.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroRealm.java new file mode 100644 index 0000000..8744ee6 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroRealm.java @@ -0,0 +1,145 @@ +package cn.abel.rest.shiro; + +import java.util.List; +import java.util.Random; +import java.util.Set; + +import cn.abel.exception.ServiceException; +import cn.abel.user.models.Permission; +import cn.abel.user.models.Role; +import cn.abel.user.models.User; +import cn.abel.user.service.PermissionService; +import cn.abel.user.service.RoleService; +import cn.abel.user.service.UserService; +import com.alibaba.dubbo.config.annotation.Reference; +import com.google.common.collect.Sets; +import org.apache.commons.collections4.CollectionUtils; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAccount; +import org.apache.shiro.authc.UsernamePasswordToken; +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.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +/** + * @date 2018/08/07 23:48 + */ +@Component +public class ShiroRealm extends AuthorizingRealm { + + /** + * logger + */ + private static final Logger logger = LoggerFactory.getLogger(ShiroRealm.class); + + @Reference(check = false) + private UserService userService; + + + /** + * 登录 认证回调函数,登录时调用 + * + * @param authenticationToken + * @return + * @throws AuthenticationException + */ + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) + throws AuthenticationException { + UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; + String username = token.getUsername(); + + Random random = new Random(); + Long userId = random.nextLong(); + + //交给AuthenticatingRealm使用CredentialsMatcher进行密码匹配 + return new SimpleAccount(new ShiroUser(userId, username, username), "", getName()); + } + + /** + * 授权 授权查询回调函数, 进行鉴权但缓存中无用户的授权信息时调用 + * + * @param principals + * @return + */ + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { + ShiroUser shiroUser = (ShiroUser) principals.getPrimaryPrincipal(); + + SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); + + Set roles = Sets.newHashSet(); + Set permissions = Sets.newHashSet(); + +// try { + //TODO 此处需要获取当前用户的所有角色和权限,放入验证器 + //登录名为超级管理员账号 +// +// //获取用户所有角色 +// User user = userService.getById( shiroUser.getUserId()); +// if (CollectionUtils.isEmpty(user.getRoles())) { +// return authorizationInfo; +// } +// +// for (Role role : user.getRoles()) { +// //停用的角色不查询权限 +// roles.add(role.getName()); +// List permissionList = permissionService.getByMap(role.getId()); +// permissionList.forEach(p -> { +// permissions.add(p.getCode()); +// }); +// } +// } catch (ServiceException e) { +// logger.error(e.getMessage()); +// return authorizationInfo; +// } + + authorizationInfo.setRoles(roles); + authorizationInfo.setStringPermissions(permissions); + + return authorizationInfo; + } + + @Override + public void clearCachedAuthorizationInfo(PrincipalCollection principals) { + super.clearCachedAuthorizationInfo(principals); + clearAllCache(); + } + + @Override + public void clearCachedAuthenticationInfo(PrincipalCollection principals) { + super.clearCachedAuthenticationInfo(principals); + clearAllCache(); + } + + @Override + public void clearCache(PrincipalCollection principals) { + super.clearCache(principals); + clearAllCache(); + } + + public void clearAllCachedAuthorizationInfo() { + if (getAuthorizationCache() != null) { + getAuthorizationCache().clear(); + } + } + + public void clearAllCachedAuthenticationInfo() { + if (getAuthenticationCache() != null) { + getAuthenticationCache().clear(); + } + + } + + public void clearAllCache() { + clearAllCachedAuthenticationInfo(); + clearAllCachedAuthorizationInfo(); + } +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroRedisCacheManager.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroRedisCacheManager.java new file mode 100644 index 0000000..03b19b7 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroRedisCacheManager.java @@ -0,0 +1,126 @@ +package cn.abel.rest.shiro; + +import java.util.Collection; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import cn.abel.rest.constants.Constants; +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.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.BoundHashOperations; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Component; + +/** + * 实现Shiro的缓存管理器CacheManger接口,将Spring应用缓存管理器注入shiro缓存管理器,这样shiro的缓存都由Spring处理 + * + * @date 2019/02/21 18:12 + */ +@Component +public class ShiroRedisCacheManager implements CacheManager { + + private final static String CACHE_KEY_PREFIX = Constants.REDIS_KEY_PRE + ":shiro:"; + + @Autowired + private ShiroProperty shiroProperty; + + @Autowired + private RedisTemplate redisTemplate; + + /** 超时时间 */ + private long expireTime; + + @Override + public Cache getCache(String name) throws CacheException { + if ("passwordRetryCache".equals(name)) { + expireTime = shiroProperty.getShiroRetryExpireTimeRedis(); + } else { + expireTime = shiroProperty.getShiroAuthorizationExpireTimeRedis(); + } + return new ShiroRedisCache(CACHE_KEY_PREFIX + name, expireTime); + } + + /** + * 为shiro量身定做的一个redis cache,为Authorization cache做了特别优化 + */ + public class ShiroRedisCache implements Cache { + + private String cacheKey; + + // 缓存的超时时间,单位为s + private long expireTime = 3600; + + public ShiroRedisCache(String cacheKey, long expireTime) { + this.cacheKey = cacheKey; + this.expireTime = expireTime; + } + + @Override + public V get(K key) throws CacheException { + BoundHashOperations hash = redisTemplate.boundHashOps(cacheKey); + Object k = hashKey(key); + return hash.get(k); + } + + @Override + public V put(K key, V value) throws CacheException { + BoundHashOperations hash = redisTemplate.boundHashOps(cacheKey); + //超时时间 + Object k = hashKey(key); + hash.put((K) k, value); + hash.expire(expireTime, TimeUnit.SECONDS); + return value; + } + + @Override + public V remove(K key) throws CacheException { + BoundHashOperations hash = redisTemplate.boundHashOps(cacheKey); + + V value = null; + try { + Object k = hashKey(key); + value = hash.get(k); + hash.delete(k); + } catch (Exception e) { + e.printStackTrace(); + } + return value; + } + + @Override + public void clear() throws CacheException { + redisTemplate.delete(cacheKey); + } + + @Override + public int size() { + BoundHashOperations hash = redisTemplate.boundHashOps(cacheKey); + return hash.size().intValue(); + } + + @Override + public Set keys() { + BoundHashOperations hash = redisTemplate.boundHashOps(cacheKey); + return hash.keys(); + } + + @Override + public Collection values() { + BoundHashOperations hash = redisTemplate.boundHashOps(cacheKey); + return hash.values(); + } + + protected Object hashKey(K key) { + //此处很重要,如果key是登录凭证,那么这是访问用户的授权缓存;将登录凭证转为user对象,返回user的id属性做为hash key,否则会以user对象做为hash key,这样就不好清除指定用户的缓存了 + if (key instanceof PrincipalCollection) { + PrincipalCollection pc = (PrincipalCollection) key; + ShiroUser user = (ShiroUser) pc.getPrimaryPrincipal(); + return user.getUserId(); + } + return key; + } + } +} \ No newline at end of file diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroUser.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroUser.java new file mode 100644 index 0000000..ea91eab --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ShiroUser.java @@ -0,0 +1,102 @@ +package cn.abel.rest.shiro; + +import java.io.Serializable; + +import com.google.common.base.Objects; + +/** + * 自定义Authentication对象,使得Subject除了携带用户的登录名外还可以携带更多信息. + * + */ +public class ShiroUser implements Serializable { + + private static final long serialVersionUID = -1373760761780840081L; + + private Long userId; + + private String loginName; + + private String nickName; + + private String token; + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public String getLoginName() { + return loginName; + } + + public void setLoginName(String loginName) { + this.loginName = loginName; + } + + public void setNickName(String nickName) { + this.nickName = nickName; + } + + public ShiroUser(Long userId, String loginName, String nickName) { + this.userId = userId; + this.loginName = loginName; + this.nickName = nickName; + } + + public String getNickName() { + return nickName; + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + /** + * 本函数输出将作为默认的输出. + */ + @Override + public String toString() { + return loginName; + } + + /** + * 重载hashCode,只计算loginName; + */ + @Override + public int hashCode() { + return Objects.hashCode(loginName); + } + + /** + * 重载equals,只计算loginName; + */ + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null) { + return false; + } + if (getClass() != obj.getClass()) { + return false; + } + ShiroUser other = (ShiroUser) obj; + if (loginName == null) { + if (other.loginName != null) { + return false; + } + } else if (!loginName.equals(other.loginName)) { + return false; + } + return true; + } + +} \ No newline at end of file diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/UuidSessionIdGenerator.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/UuidSessionIdGenerator.java new file mode 100644 index 0000000..53d4c59 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/UuidSessionIdGenerator.java @@ -0,0 +1,20 @@ +package cn.abel.rest.shiro; + +import java.io.Serializable; +import java.util.UUID; + +import org.apache.shiro.session.Session; +import org.apache.shiro.session.mgt.eis.SessionIdGenerator; + +/** + * 会话ID生成器 + * + * @date 2018/08/08 11:04 + */ +public class UuidSessionIdGenerator implements SessionIdGenerator { + + @Override + public Serializable generateId(Session session) { + return UUID.randomUUID().toString().replace("-", "").toUpperCase(); + } +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/credentials/PasswordHelper.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/credentials/PasswordHelper.java new file mode 100644 index 0000000..663c312 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/credentials/PasswordHelper.java @@ -0,0 +1,27 @@ +package cn.abel.rest.shiro.credentials; + +import org.apache.shiro.crypto.hash.SimpleHash; +import org.apache.shiro.util.ByteSource; + +/** + * 密码 + * + */ +public class PasswordHelper { + + private static String algorithmName = "md5"; + private static int hashIterations = 2; + + public void setAlgorithmName(String algorithmName) { + PasswordHelper.algorithmName = algorithmName; + } + + public void setHashIterations(int hashIterations) { + PasswordHelper.hashIterations = hashIterations; + } + + public static String encryptPassword(String userName, String password) { + return new SimpleHash(algorithmName, password, ByteSource.Util.bytes(userName), hashIterations).toHex(); + } + +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/credentials/RetryLimitHashedCredentialsMatcher.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/credentials/RetryLimitHashedCredentialsMatcher.java new file mode 100644 index 0000000..959e5e9 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/credentials/RetryLimitHashedCredentialsMatcher.java @@ -0,0 +1,118 @@ +package cn.abel.rest.shiro.credentials; + +import java.util.concurrent.atomic.AtomicInteger; +import cn.abel.rest.shiro.ShiroUser; +import cn.abel.rest.shiro.ShiroProperty; + +import cn.abel.user.service.UserService; +import com.alibaba.dubbo.config.annotation.Reference; +import org.apache.commons.lang3.StringUtils; +import org.apache.shiro.authc.*; +import org.apache.shiro.authc.credential.HashedCredentialsMatcher; +import org.apache.shiro.cache.Cache; +import org.apache.shiro.cache.CacheManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; + +import javax.websocket.Session; + +/** + * 登录验证 + */ +public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher { + private static final Logger logger = LoggerFactory.getLogger(RetryLimitHashedCredentialsMatcher.class); + + @Autowired + private ShiroProperty shiroProperty; + @Reference + private UserService userService; + + private Cache passwordRetryCache; + + public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) { + passwordRetryCache = cacheManager.getCache("passwordRetryCache"); + } + + @Override + public boolean doCredentialsMatch(AuthenticationToken originToken, AuthenticationInfo info) { + ThirdPartySupportedToken token = (ThirdPartySupportedToken) originToken; + ShiroUser shiroUser = (ShiroUser) info.getPrincipals().getPrimaryPrincipal(); + + //优先使用第三方token进行登录验证。 + if (StringUtils.isNotBlank(token.getAccessToken())) { + Session session = null; +// try { + //TODO 从用户服务中通过 token 获取 session +// session = userService.getSessionByToken(token.getAccessToken()); +// } catch (ServiceException e) { +// throw new ExpiredCredentialsException(); +// } + if (session == null) { + throw new ExpiredCredentialsException(); + } + //TODO 从session 中获取用户相关信息,放入shiro 验证对象 shiroUser +// shiroUser.setUserId(session.getUser().getProfileId()); +// shiroUser.setLoginName(session.getCurrentLoginName()); +// shiroUser.setNickName(session.getUser().getNickName()); + return true; + } + + String username = token.getUsername(); + String password = String.valueOf(token.getPassword()); + + //判断该账号登录重试次数是否超过上限。 + AtomicInteger retryCount = passwordRetryCache.get(username); + if (retryCount != null && retryCount.get() >= shiroProperty.getShiroRetryMax()) { + throw new ExcessiveAttemptsException(); + } + + //TODO 没有token,则调用用户服务的登录接口重新登录 获取 session +// Login login = new Login(); +// login.setIp(Constants.CLIENT_IP); +// login.setLoginMode(2); +// login.setLoginName(username); +// login.setLoginType(3); +// login.setPassword(password); +// try { +// Session session = userService.login(login); +// if (session == null) { +// logger.error("登录失败。username={}", username); +// throw new IncorrectCredentialsException(); +// } +// if (session.getUser().getDeleteFlag() != 1) { +// logger.error("登录失败。用户已被锁定。username={}", username); +// throw new LockedAccountException(); +// } + + //登录成功,清空登录次数缓存。 + if (retryCount != null) { + passwordRetryCache.remove(username); + } + //TODO 将 session 中的用户id 信息放入 shiro 对shiroUser象中 +// shiroUser.setUserId(session.getUser().getProfileId()); +// return true; +// } catch (ServiceException e) { + //TODO 根据登录接口的异常信息判读 失败情况 +// if (e.getErrorCode() == InfoCode.USER_LOGIN_NOT_EXIST.getStatus() +// || e.getErrorCode() == InfoCode.REQUEST_PARAM_ERROR.getStatus()) { +// logger.error("登录失败,用户不存在。username={}", username); +// throw new UnknownAccountException(); +// } else if (e.getErrorCode() == InfoCode.PASSWORD_ERROR.getStatus()) { +// //密码错误缓存登录重试次数缓存。 +// if (retryCount == null) { +// retryCount = new AtomicInteger(0); +// } +// retryCount.incrementAndGet(); +// passwordRetryCache.put(username, retryCount); +// +// logger.error("登录失败,用户名或密码错误。username={}", username); +// throw new IncorrectCredentialsException(); +// } else { +// logger.error("登录失败,msg={}", e.getMessage()); +// } +// } + + return false; + } +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/credentials/ThirdPartySupportedToken.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/credentials/ThirdPartySupportedToken.java new file mode 100644 index 0000000..79b9b48 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/credentials/ThirdPartySupportedToken.java @@ -0,0 +1,92 @@ +package cn.abel.rest.shiro.credentials; + +import org.apache.shiro.authc.UsernamePasswordToken; +import org.springframework.stereotype.Component; + +/** + * 支持第三方登录的shiro token。 + */ +public class ThirdPartySupportedToken extends UsernamePasswordToken { + private String username; + private char[] password; + private boolean rememberMe = false; + private String host; + + /** + * 第三方token。 + */ + private String accessToken; + + public ThirdPartySupportedToken( + String username, + String password, + String accessToken) { + this.username = username; + this.password = password != null ? password.toCharArray() : null; + this.accessToken = accessToken; + } + + @Override + public String getUsername() { + return this.username; + } + + @Override + public void setUsername(String username) { + this.username = username; + } + + @Override + public char[] getPassword() { + return this.password; + } + + @Override + public void setPassword(char[] password) { + this.password = password; + } + + @Override + public Object getPrincipal() { + return this.getUsername(); + } + + @Override + public Object getCredentials() { + return this.getPassword(); + } + + @Override + public String getHost() { + return this.host; + } + + @Override + public void setHost(String host) { + this.host = host; + } + + @Override + public boolean isRememberMe() { + return this.rememberMe; + } + + @Override + public void setRememberMe(boolean rememberMe) { + this.rememberMe = rememberMe; + } + + public String getAccessToken() { + return accessToken; + } + + public void setAccessToken(String accessToken) { + this.accessToken = accessToken; + } + + @Override + public void clear() { + super.clear(); + this.accessToken = null; + } +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ext/QuartzSessionValidationJob.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ext/QuartzSessionValidationJob.java new file mode 100644 index 0000000..56ec875 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ext/QuartzSessionValidationJob.java @@ -0,0 +1,63 @@ +package cn.abel.rest.shiro.ext; + +import org.apache.shiro.session.mgt.ValidatingSessionManager; +import org.quartz.Job; +import org.quartz.JobDataMap; +import org.quartz.JobExecutionContext; +import org.quartz.JobExecutionException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 基于Quartz 2.* 版本的实现 + * + * @author web + */ +public class QuartzSessionValidationJob implements Job { + + /** + * Key used to store the session manager in the job data map for this job. + */ + public static final String SESSION_MANAGER_KEY = "sessionManager"; + + /**-------------------------------------------- + | I N S T A N C E V A R I A B L E S | + ============================================*/ + private static final Logger log = LoggerFactory.getLogger(QuartzSessionValidationJob.class); + + /*-------------------------------------------- + | C O N S T R U C T O R S | + ============================================*/ + + /*-------------------------------------------- + | A C C E S S O R S / M O D I F I E R S | + ============================================*/ + + /*-------------------------------------------- + | M E T H O D S | + ============================================*/ + + /** + * Called when the job is executed by quartz. This method delegates to the validateSessions() method on the + * associated session manager. + * + * @param context the Quartz job execution context for this execution. + */ + @Override + public void execute(JobExecutionContext context) throws JobExecutionException { + + JobDataMap jobDataMap = context.getMergedJobDataMap(); + ValidatingSessionManager sessionManager = (ValidatingSessionManager) jobDataMap.get(SESSION_MANAGER_KEY); + + if (log.isDebugEnabled()) { + log.debug("Executing session validation Quartz job..."); + } + + sessionManager.validateSessions(); + + if (log.isDebugEnabled()) { + log.debug("Session validation Quartz job complete."); + } + } + +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ext/QuartzSessionValidationScheduler.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ext/QuartzSessionValidationScheduler.java new file mode 100644 index 0000000..323511b --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/ext/QuartzSessionValidationScheduler.java @@ -0,0 +1,156 @@ +package cn.abel.rest.shiro.ext; + +import org.apache.shiro.session.mgt.DefaultSessionManager; +import org.apache.shiro.session.mgt.SessionValidationScheduler; +import org.apache.shiro.session.mgt.ValidatingSessionManager; +import org.quartz.JobBuilder; +import org.quartz.JobDetail; +import org.quartz.Scheduler; +import org.quartz.SchedulerException; +import org.quartz.SimpleScheduleBuilder; +import org.quartz.SimpleTrigger; +import org.quartz.TriggerBuilder; +import org.quartz.TriggerKey; +import org.quartz.impl.StdSchedulerFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 基于Quartz 2.* 版本的实现 + * + * @author web + */ +public class QuartzSessionValidationScheduler implements SessionValidationScheduler { + + public static final long DEFAULT_SESSION_VALIDATION_INTERVAL = + DefaultSessionManager.DEFAULT_SESSION_VALIDATION_INTERVAL; + private static final String JOB_NAME = "SessionValidationJob"; + private static final Logger log = LoggerFactory.getLogger(QuartzSessionValidationScheduler.class); + private static final String SESSION_MANAGER_KEY = QuartzSessionValidationJob.SESSION_MANAGER_KEY; + private Scheduler scheduler; + private boolean schedulerImplicitlyCreated = false; + + private boolean enabled = false; + private ValidatingSessionManager sessionManager; + private long sessionValidationInterval = DEFAULT_SESSION_VALIDATION_INTERVAL; + + public QuartzSessionValidationScheduler() { + } + + public QuartzSessionValidationScheduler(ValidatingSessionManager sessionManager) { + this.sessionManager = sessionManager; + } + + protected Scheduler getScheduler() throws SchedulerException { + if (this.scheduler == null) { + this.scheduler = StdSchedulerFactory.getDefaultScheduler(); + this.schedulerImplicitlyCreated = true; + } + return this.scheduler; + } + + public void setScheduler(Scheduler scheduler) { + this.scheduler = scheduler; + } + + public void setSessionManager(ValidatingSessionManager sessionManager) { + this.sessionManager = sessionManager; + } + + @Override + public boolean isEnabled() { + return this.enabled; + } + + public void setSessionValidationInterval(long sessionValidationInterval) { + this.sessionValidationInterval = sessionValidationInterval; + } + + @Override + public void enableSessionValidation() { + if (log.isDebugEnabled()) { + log.debug("Scheduling session validation job using Quartz with session validation interval of [" + this.sessionValidationInterval + "]ms..."); + } + + try { + //Quartz 2中的实现 + SimpleTrigger trigger = TriggerBuilder.newTrigger().startNow().withIdentity(JOB_NAME, + Scheduler.DEFAULT_GROUP).withSchedule(SimpleScheduleBuilder.simpleSchedule().withIntervalInMilliseconds(sessionValidationInterval)).build(); + + JobDetail detail = JobBuilder.newJob(QuartzSessionValidationJob.class).withIdentity(JOB_NAME, + Scheduler.DEFAULT_GROUP).build(); + detail.getJobDataMap().put(SESSION_MANAGER_KEY, this.sessionManager); + Scheduler scheduler = getScheduler(); + + scheduler.scheduleJob(detail, trigger); + if (this.schedulerImplicitlyCreated) { + scheduler.start(); + if (log.isDebugEnabled()) { + log.debug("Successfully started implicitly created Quartz Scheduler instance."); + } + } + this.enabled = true; + + if (log.isDebugEnabled()) { + log.debug("Session validation job successfully scheduled with Quartz."); + } + } catch (SchedulerException e) { + if (log.isErrorEnabled()) { + log.error("Error starting the Quartz session validation job. Session validation may not occur.", e); + } + } + } + + @Override + public void disableSessionValidation() { + if (log.isDebugEnabled()) { + log.debug("Stopping Quartz session validation job..."); + } + Scheduler scheduler; + try { + scheduler = getScheduler(); + if (scheduler == null) { + if (log.isWarnEnabled()) { + log.warn("getScheduler() method returned a null Quartz scheduler, which is unexpected. Please " + + "check your configuration and/or implementation. Returning quietly since there is no " + + "validation job to remove (scheduler does not exist)."); + } + + return; + } + } catch (SchedulerException e) { + if (log.isWarnEnabled()) { + log.warn("Unable to acquire Quartz Scheduler. Ignoring and returning (already stopped?)", e); + } + return; + } + try { + scheduler.unscheduleJob(new TriggerKey("SessionValidationJob", "DEFAULT")); + if (log.isDebugEnabled()) { + log.debug("Quartz session validation job stopped successfully."); + } + } catch (SchedulerException e) { + if (log.isDebugEnabled()) { + log.debug("Could not cleanly remove SessionValidationJob from Quartz scheduler. Ignoring and " + + "stopping.", e); + } + + } + + this.enabled = false; + + if (this.schedulerImplicitlyCreated) { + try { + scheduler.shutdown(); + } catch (SchedulerException e) { + if (log.isWarnEnabled()) { + log.warn("Unable to cleanly shutdown implicitly created Quartz Scheduler instance.", e); + } + } finally { + setScheduler(null); + this.schedulerImplicitlyCreated = false; + } + } + } + +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/filter/ShiroFormAuthenticationFilter.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/filter/ShiroFormAuthenticationFilter.java new file mode 100644 index 0000000..2804e71 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/filter/ShiroFormAuthenticationFilter.java @@ -0,0 +1,160 @@ +package cn.abel.rest.shiro.filter; + +import java.io.IOException; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; + +import cn.abel.code.InfoCode; +import cn.abel.response.ResponseEntity; +import cn.abel.rest.constants.Constants; +import cn.abel.rest.shiro.ShiroUser; +import cn.abel.rest.shiro.credentials.ThirdPartySupportedToken; +import com.alibaba.fastjson.JSON; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.shiro.authc.*; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.web.filter.authc.FormAuthenticationFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.MediaType; + +/** + * 登录filter + * + */ +public class ShiroFormAuthenticationFilter extends FormAuthenticationFilter { + private static final Logger logger = LoggerFactory.getLogger(ShiroFormAuthenticationFilter.class); + + @Override + public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception { + //登录请求用户名和accessToken不能同时为空。 + if (isLoginRequest(request, response)) { + if (StringUtils.isBlank(getUsername(request)) + && StringUtils.isBlank(request.getParameter(Constants.THIRD_PARTY_ACCESS_TOKEN_NAME))) { + return responseDirectly((HttpServletResponse) response, + ResponseEntity.error(InfoCode.REQUEST_PARAM_ERROR)); + } + } + return super.onPreHandle(request, response, mappedValue); + } + + /** + * 当鉴权失败的时候执行的方法。 + * + * @param request + * @param response + * @return + * @throws Exception + */ + @Override + protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { + if (isLoginRequest(request, response)) { + if (isLoginSubmission(request, response)) { + return executeLogin(request, response); + } + //登录仅限HttpPost方式,其他任何方式是不合法的。 + return responseDirectly((HttpServletResponse) response, ResponseEntity.error(InfoCode.LOGIN_TYPE_ERROR)); + } + //调用非登录方法时认证失败的情况。 + return responseDirectly((HttpServletResponse) response, ResponseEntity.error(InfoCode.INVALID_TOKEN)); + } + + /** + * 登录验证失败后执行的方法。 + * + * @param token + * @param e + * @param request + * @param response + * @return + */ + @Override + protected boolean onLoginFailure( + AuthenticationToken token, + AuthenticationException e, + ServletRequest request, + ServletResponse response) { + HttpServletResponse resp = (HttpServletResponse) response; + if (e instanceof ExcessiveAttemptsException) { + return responseDirectly(resp, ResponseEntity.error(InfoCode.PASSWORD_ERROR_MORE_THAN)); + } else if (e instanceof UnknownAccountException) { + return responseDirectly(resp, ResponseEntity.error(InfoCode.PASSWORD_ERROR)); + } else if (e instanceof IncorrectCredentialsException) { + return responseDirectly(resp, ResponseEntity.error(InfoCode.PASSWORD_ERROR)); + } else if (e instanceof LockedAccountException) { + return responseDirectly(resp, ResponseEntity.error(InfoCode.USER_PROFILE_LOCK)); + } else if (e instanceof ExpiredCredentialsException) { + return responseDirectly(resp, ResponseEntity.error(InfoCode.INVALID_LOGIN)); + } else { + return responseDirectly(resp, ResponseEntity.error(InfoCode.SERVICE_UNAVAILABLE)); + } + } + + /** + * 登录成功后执行的方法。 + * + * @param token + * @param subject + * @param request + * @param response + * @return + * @throws Exception + */ + @Override + protected boolean onLoginSuccess( + AuthenticationToken token, + Subject subject, + ServletRequest request, + ServletResponse response) throws Exception { + ShiroUser user = (ShiroUser) subject.getPrincipal(); + user.setToken(subject.getSession().getId().toString()); + return true; + } + + /** + * 创建包含第三方accessToken的shiro token。 + * + * @param request + * @param response + * @return + */ + @Override + protected AuthenticationToken createToken(ServletRequest request, ServletResponse response) { + String username = getUsername(request); + String password = getPassword(request); + String accessToken = request.getParameter(Constants.THIRD_PARTY_ACCESS_TOKEN_NAME); + //shiro后续流程可能会用到username,所以如果用accessToken登录时赋值username为它的值。 + if (StringUtils.isBlank(username)) { + username = accessToken; + } + return new ThirdPartySupportedToken(username, password, accessToken); + } + + /** + * 直接构造HttpResponse,不再执行后续的所有方法。 + * + * @param response + * @param entity + * @return + */ + private boolean responseDirectly(HttpServletResponse response, ResponseEntity entity) { + response.reset(); + response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + + //设置跨域信息。 + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Credentials", "true"); + response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, PUT, OPTIONS, HEAD"); + response.setHeader("Access-Control-Max-Age", "3600"); + response.setHeader("Access-Control-Allow-Headers", "*"); + try { + response.getWriter().write(JSON.toJSONString(entity)); + } catch (IOException e) { + logger.error("ResponseError ex:{}", ExceptionUtils.getStackTrace(e)); + } + return false; + } +} diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/shiro/filter/ShiroLogoutFilter.java b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/filter/ShiroLogoutFilter.java new file mode 100644 index 0000000..80b0d10 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/shiro/filter/ShiroLogoutFilter.java @@ -0,0 +1,26 @@ +package cn.abel.rest.shiro.filter; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import org.apache.shiro.subject.Subject; +import org.apache.shiro.web.filter.authc.LogoutFilter; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * 注销filter + * + * @date 2019/02/26 13:32 + */ +public class ShiroLogoutFilter extends LogoutFilter { + + private static final Logger logger = LoggerFactory.getLogger(ShiroLogoutFilter.class); + + @Override + protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { + Subject subject = getSubject(request, response); + subject.logout(); + return true; + } +} \ No newline at end of file diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/utils/ServletKit.java b/springboot-shiro2/src/main/java/cn/abel/rest/utils/ServletKit.java new file mode 100644 index 0000000..090d00c --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/utils/ServletKit.java @@ -0,0 +1,36 @@ +package cn.abel.rest.utils; + +import javax.servlet.http.HttpServletRequest; + +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; +import org.springframework.web.servlet.resource.ResourceUrlProvider; + +/** + * Servlet工具类 + * + * @date 2019/02/22 14:07 + */ +public class ServletKit { + + /** + * 在任意位置获取Request + * + * @return + */ + public static HttpServletRequest getRequest() { + return ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); + } + + /** + * 判断请求uri是否是静态资源方法 + * + * @param uri + * @return + */ + public static boolean isStaticFile(String uri) { + ResourceUrlProvider resourceUrlProvider = SpringContextKit.getBean(ResourceUrlProvider.class); + String staticUri = resourceUrlProvider.getForLookupPath(uri); + return staticUri != null; + } +} \ No newline at end of file diff --git a/springboot-shiro2/src/main/java/cn/abel/rest/utils/SpringContextKit.java b/springboot-shiro2/src/main/java/cn/abel/rest/utils/SpringContextKit.java new file mode 100644 index 0000000..7633886 --- /dev/null +++ b/springboot-shiro2/src/main/java/cn/abel/rest/utils/SpringContextKit.java @@ -0,0 +1,60 @@ +package cn.abel.rest.utils; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.stereotype.Component; + +/** + * 这个类是为了解决在普通类调用service的问题 + * + * @date 2018/10/24 15:16 + * @content OfflineMessageService offlineMessageService = (OfflineMessageService) SpringContextUtil + * .getBean("offlineMessageService"); + */ +@Component +public class SpringContextKit implements ApplicationContextAware { + + /** Spring应用上下文 */ + private static ApplicationContext applicationContext; + + /** 下面的这个方法上加了@Override注解,原因是继承ApplicationContextAware接口是必须实现的方法 */ + @Override + public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { + SpringContextKit.applicationContext = applicationContext; + } + + public static ApplicationContext getApplicationContext() { + return applicationContext; + } + + public static Object getBean(String name) throws BeansException { + return applicationContext.getBean(name); + } + + public static Object getBean(String name, Class requiredType) throws BeansException { + + return applicationContext.getBean(name, requiredType); + } + + public static T getBean(Class clazz) throws BeansException { + return applicationContext.getBean(clazz); + } + + public static boolean containsBean(String name) { + return applicationContext.containsBean(name); + } + + public static boolean isSingleton(String name) throws NoSuchBeanDefinitionException { + return applicationContext.isSingleton(name); + } + + public static Class getType(String name) throws NoSuchBeanDefinitionException { + return applicationContext.getType(name); + } + + public static String[] getAliases(String name) throws NoSuchBeanDefinitionException { + return applicationContext.getAliases(name); + } +} \ No newline at end of file diff --git a/springboot-shiro2/src/main/resources/META-INF/spring/consumer.xml b/springboot-shiro2/src/main/resources/META-INF/spring/consumer.xml new file mode 100644 index 0000000..a3cf9cf --- /dev/null +++ b/springboot-shiro2/src/main/resources/META-INF/spring/consumer.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/springboot-shiro2/src/main/resources/dev/application.properties b/springboot-shiro2/src/main/resources/dev/application.properties new file mode 100644 index 0000000..b2be9c2 --- /dev/null +++ b/springboot-shiro2/src/main/resources/dev/application.properties @@ -0,0 +1,94 @@ +## tomcat\u914D\u7F6E +server.servlet.context-path=/shiro +server.port=9738 +#server.tomcat.maxHttpHeaderSize=8192 +server.tomcat.uri-encoding=UTF-8 +spring.http.encoding.charset=UTF-8 +spring.http.encoding.enabled=true +spring.http.encoding.force=true +spring.messages.encoding=UTF-8 +# tomcat\u6700\u5927\u7EBF\u7A0B\u6570\uFF0C\u9ED8\u8BA4\u4E3A200 +server.tomcat.max-threads=800 +# session\u6700\u5927\u8D85\u65F6\u65F6\u95F4(\u5206\u949F)\uFF0C\u9ED8\u8BA4\u4E3A30 +server.session-timeout=60 + +## spring \u914D\u7F6E +spring.application.name=springboot-shiro +application.main=cn.abel.rest.ShiroRestApplication + +## LOG +logging.file=./logs/springboot-shiro.log + +## dubbo \u914D\u7F6E +dubbo.application.name=springboot-shiro +dubbo.registry.group=abel +#dubbo.registry.address=127.0.0.1:2181 +dubbo.registry.address=127.0.0.1:2181 +dubbo.registry.version=1.0.0 +dubbo.protocol.port=13182 +dubbo.annotation.package=cn.abel.rest +dubbo.log.file=./logs/springboot-shiro-dubbo.log + +## spring cache +#\u7F13\u5B58\u7684\u540D\u79F0\u96C6\u5408\uFF0C\u591A\u4E2A\u91C7\u7528\u9017\u53F7\u5206\u5272 +#spring.cache.cache-names=admin,role +#\u7F13\u5B58\u7684\u7C7B\u578B\uFF0C\u5B98\u65B9\u63D0\u4F9B\u4E86\u5F88\u591A\uFF0C\u8FD9\u91CC\u6211\u4EEC\u586B\u5199redis +spring.cache.type=redis +#\u662F\u5426\u7F13\u5B58null\u6570\u636E\uFF0C\u9ED8\u8BA4\u662Ffalse +#spring.cache.redis.cache-null-values=false +#redis\u4E2D\u7F13\u5B58\u8D85\u65F6\u7684\u65F6\u95F4\uFF0C\u9ED8\u8BA460000ms +#spring.cache.redis.time-to-live=60000 +#\u7F13\u5B58\u6570\u636Ekey\u662F\u5426\u4F7F\u7528\u524D\u7F00\uFF0C\u9ED8\u8BA4\u662Ftrue +#spring.cache.redis.use-key-prefix=true + +## redis \u914D\u7F6E +# Redis\u6570\u636E\u5E93\u7D22\u5F15 +spring.redis.database=3 +# Redis\u670D\u52A1\u5668\u5730\u5740 +spring.redis.host=127.0.0.1 +# Redis\u670D\u52A1\u5668\u8FDE\u63A5\u7AEF\u53E3 +spring.redis.port=6379 +# Redis\u670D\u52A1\u5668\u8FDE\u63A5\u5BC6\u7801\uFF08\u9ED8\u8BA4\u4E3A\u7A7A\uFF09 +spring.redis.password= +# \u8FDE\u63A5\u6C60\u6700\u5927\u8FDE\u63A5\u6570\uFF08\u4F7F\u7528\u8D1F\u503C\u8868\u793A\u6CA1\u6709\u9650\u5236\uFF09 +spring.redis.lettuce.pool.max-active=100 +# \u8FDE\u63A5\u6C60\u6700\u5927\u963B\u585E\u7B49\u5F85\u65F6\u95F4\uFF08\u4F7F\u7528\u8D1F\u503C\u8868\u793A\u6CA1\u6709\u9650\u5236\uFF09 +spring.redis.lettuce.pool.max-wait=1000 +# \u8FDE\u63A5\u6C60\u4E2D\u7684\u6700\u5927\u7A7A\u95F2\u8FDE\u63A5 +spring.redis.lettuce.pool.max-idle=50 +# \u8FDE\u63A5\u6C60\u4E2D\u7684\u6700\u5C0F\u7A7A\u95F2\u8FDE\u63A5 +spring.redis.lettuce.pool.min-idle=0 +# \u8FDE\u63A5\u8D85\u65F6\u65F6\u95F4\uFF08\u6BEB\u79D2\uFF09 +spring.redis.lettuce.shutdown-timeout=0 + +## view\u914D\u7F6E +# FreeMarker\u914D\u7F6E +# \u662F\u5426\u5F00\u542F\u6A21\u677F\u7F13\u5B58 +spring.freemarker.cache=false +# \u7F16\u7801\u683C\u5F0F +spring.freemarker.charset=UTF-8 +# \u6A21\u677F\u7684\u5A92\u4F53\u7C7B\u578B\u8BBE\u7F6E +spring.freemarker.content-type=text/html +# \u524D\u7F00\u8BBE\u7F6E \u9ED8\u8BA4\u4E3A "" +spring.freemarker.prefix= +# \u540E\u7F00\u8BBE\u7F6E \u9ED8\u8BA4\u4E3A .ftl +spring.freemarker.suffix=.ftl +# +#spring.freemarker.viewClass=com.yekai.demo.admin.freemarker.CustomFreeMarkerView +# +spring.freemarker.expose-request-attributes=true +spring.freemarker.expose-session-attributes=true +# \u5F00\u542F\u5B8F\u652F\u6301 +spring.freemarker.expose-spring-macro-helpers=true +# ftl\u4E2D\u83B7\u53D6request +spring.freemarker.request-context-attribute=request + +#\u767B\u5F55\u91CD\u8BD5\u6B21\u6570 +shiro.retryMax=5 +#\u767B\u5F55\u5931\u8D25\u9501\u5B9A\u65F6\u95F4\uFF08\u79D2\uFF09 +shiro.retryExpireTimeRedis=900 +#\u6388\u6743\u8D85\u65F6\u65F6\u95F4\uFF08\u79D2\uFF09 +shiro.authorizationExpireTimeRedis=3600 +#session\u8D85\u65F6\u65F6\u95F4\uFF08\u79D2\uFF09 +shiro.sessionExpireTimeRedis=3600 + diff --git a/springboot-shiro2/src/main/resources/dev/banner.txt b/springboot-shiro2/src/main/resources/dev/banner.txt new file mode 100644 index 0000000..07d3606 --- /dev/null +++ b/springboot-shiro2/src/main/resources/dev/banner.txt @@ -0,0 +1,5 @@ +######################################################## +# # +# dev # +# # +######################################################## \ No newline at end of file diff --git a/springboot-shiro2/src/main/resources/dev/logback-spring.xml b/springboot-shiro2/src/main/resources/dev/logback-spring.xml new file mode 100644 index 0000000..92810cd --- /dev/null +++ b/springboot-shiro2/src/main/resources/dev/logback-spring.xml @@ -0,0 +1,41 @@ + + + springboot-shiro + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + ${LOG_FILE} + + + ${LOG_FILE}.%d{yyyy-MM-dd} + + 60 + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%method - %msg%n + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/springboot-shiro2/src/main/resources/local/application.properties b/springboot-shiro2/src/main/resources/local/application.properties new file mode 100644 index 0000000..879dfe0 --- /dev/null +++ b/springboot-shiro2/src/main/resources/local/application.properties @@ -0,0 +1,94 @@ +## tomcat\u914D\u7F6E +server.servlet.context-path=/shiro +server.port=9738 +#server.tomcat.maxHttpHeaderSize=8192 +server.tomcat.uri-encoding=UTF-8 +spring.http.encoding.charset=UTF-8 +spring.http.encoding.enabled=true +spring.http.encoding.force=true +spring.messages.encoding=UTF-8 +# tomcat\u6700\u5927\u7EBF\u7A0B\u6570\uFF0C\u9ED8\u8BA4\u4E3A200 +server.tomcat.max-threads=800 +# session\u6700\u5927\u8D85\u65F6\u65F6\u95F4(\u5206\u949F)\uFF0C\u9ED8\u8BA4\u4E3A30 +server.session-timeout=60 + +## spring \u914D\u7F6E +spring.application.name=springboot-shiro +application.main=cn.abel.rest.ShiroRestApplication + +## LOG +logging.file=./logs/springboot-shiro.log + +## dubbo \u914D\u7F6E +dubbo.application.name=springboot-shiro +dubbo.registry.group=abel +#dubbo.registry.address=127.0.0.1:2181 +dubbo.registry.address=127.0.0.1:2181 +dubbo.registry.version=1.0.0 +dubbo.protocol.port=13182 +dubbo.annotation.package=cn.abel.rest +dubbo.log.file=./logs/springboot-shiro-dubbo.log + +## spring cache +#\u7F13\u5B58\u7684\u540D\u79F0\u96C6\u5408\uFF0C\u591A\u4E2A\u91C7\u7528\u9017\u53F7\u5206\u5272 +#spring.cache.cache-names=admin,role +#\u7F13\u5B58\u7684\u7C7B\u578B\uFF0C\u5B98\u65B9\u63D0\u4F9B\u4E86\u5F88\u591A\uFF0C\u8FD9\u91CC\u6211\u4EEC\u586B\u5199redis +spring.cache.type=redis +#\u662F\u5426\u7F13\u5B58null\u6570\u636E\uFF0C\u9ED8\u8BA4\u662Ffalse +#spring.cache.redis.cache-null-values=false +#redis\u4E2D\u7F13\u5B58\u8D85\u65F6\u7684\u65F6\u95F4\uFF0C\u9ED8\u8BA460000ms +#spring.cache.redis.time-to-live=60000 +#\u7F13\u5B58\u6570\u636Ekey\u662F\u5426\u4F7F\u7528\u524D\u7F00\uFF0C\u9ED8\u8BA4\u662Ftrue +#spring.cache.redis.use-key-prefix=true + +## redis \u914D\u7F6E +# Redis\u6570\u636E\u5E93\u7D22\u5F15 +spring.redis.database=3 +# Redis\u670D\u52A1\u5668\u5730\u5740 +spring.redis.host=127.0.0.1 +# Redis\u670D\u52A1\u5668\u8FDE\u63A5\u7AEF\u53E3 +spring.redis.port=6379 +# Redis\u670D\u52A1\u5668\u8FDE\u63A5\u5BC6\u7801\uFF08\u9ED8\u8BA4\u4E3A\u7A7A\uFF09 +spring.redis.password= +# \u8FDE\u63A5\u6C60\u6700\u5927\u8FDE\u63A5\u6570\uFF08\u4F7F\u7528\u8D1F\u503C\u8868\u793A\u6CA1\u6709\u9650\u5236\uFF09 +spring.redis.lettuce.pool.max-active=100 +# \u8FDE\u63A5\u6C60\u6700\u5927\u963B\u585E\u7B49\u5F85\u65F6\u95F4\uFF08\u4F7F\u7528\u8D1F\u503C\u8868\u793A\u6CA1\u6709\u9650\u5236\uFF09 +spring.redis.lettuce.pool.max-wait=1000 +# \u8FDE\u63A5\u6C60\u4E2D\u7684\u6700\u5927\u7A7A\u95F2\u8FDE\u63A5 +spring.redis.lettuce.pool.max-idle=50 +# \u8FDE\u63A5\u6C60\u4E2D\u7684\u6700\u5C0F\u7A7A\u95F2\u8FDE\u63A5 +spring.redis.lettuce.pool.min-idle=0 +# \u8FDE\u63A5\u8D85\u65F6\u65F6\u95F4\uFF08\u6BEB\u79D2\uFF09 +spring.redis.lettuce.shutdown-timeout=0 + +## view\u914D\u7F6E +# FreeMarker\u914D\u7F6E +# \u662F\u5426\u5F00\u542F\u6A21\u677F\u7F13\u5B58 +spring.freemarker.cache=false +# \u7F16\u7801\u683C\u5F0F +spring.freemarker.charset=UTF-8 +# \u6A21\u677F\u7684\u5A92\u4F53\u7C7B\u578B\u8BBE\u7F6E +spring.freemarker.content-type=text/html +# \u524D\u7F00\u8BBE\u7F6E \u9ED8\u8BA4\u4E3A "" +spring.freemarker.prefix= +# \u540E\u7F00\u8BBE\u7F6E \u9ED8\u8BA4\u4E3A .ftl +spring.freemarker.suffix=.ftl +# +#spring.freemarker.viewClass=com.yekai.demo.admin.freemarker.CustomFreeMarkerView +# +spring.freemarker.expose-request-attributes=true +spring.freemarker.expose-session-attributes=true +# \u5F00\u542F\u5B8F\u652F\u6301 +spring.freemarker.expose-spring-macro-helpers=true +# ftl\u4E2D\u83B7\u53D6request +spring.freemarker.request-context-attribute=request + +#\u767B\u5F55\u91CD\u8BD5\u6B21\u6570 +shiro.retryMax=5 +#\u767B\u5F55\u5931\u8D25\u9501\u5B9A\u65F6\u95F4\uFF08\u79D2\uFF09 +shiro.retryExpireTimeRedis=900 +#\u6388\u6743\u8D85\u65F6\u65F6\u95F4\uFF08\u79D2\uFF09 +shiro.authorizationExpireTimeRedis=3600 +#session\u8D85\u65F6\u65F6\u95F4\uFF08\u79D2\uFF09 +shiro.sessionExpireTimeRedis=3600 + diff --git a/springboot-shiro2/src/main/resources/local/banner.txt b/springboot-shiro2/src/main/resources/local/banner.txt new file mode 100644 index 0000000..42f7c59 --- /dev/null +++ b/springboot-shiro2/src/main/resources/local/banner.txt @@ -0,0 +1,5 @@ +######################################################## +# # +# local # +# # +######################################################## \ No newline at end of file diff --git a/springboot-shiro2/src/main/resources/local/logback-spring.xml b/springboot-shiro2/src/main/resources/local/logback-spring.xml new file mode 100644 index 0000000..1787857 --- /dev/null +++ b/springboot-shiro2/src/main/resources/local/logback-spring.xml @@ -0,0 +1,41 @@ + + + springboot-shiro + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + ${LOG_FILE} + + + ${LOG_FILE}.%d{yyyy-MM-dd} + + 60 + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36}.%method - %msg%n + + + + + + + + + + + + + + + + + + \ No newline at end of file