diff --git a/SpringBootShiroJWT/SpringBootShiroJWT.iml b/SpringBootShiroJWT/SpringBootShiroJWT.iml new file mode 100644 index 0000000..e753cc9 --- /dev/null +++ b/SpringBootShiroJWT/SpringBootShiroJWT.iml @@ -0,0 +1,105 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/SpringBootShiroJWT/pom.xml b/SpringBootShiroJWT/pom.xml new file mode 100644 index 0000000..8c12464 --- /dev/null +++ b/SpringBootShiroJWT/pom.xml @@ -0,0 +1,82 @@ + + + + SpringBoot2 + zz + 0.0.1-SNAPSHOT + + 4.0.0 + + SpringBootShiroJWT + + + + UTF-8 + UTF-8 + 1.8 + 1.4.0 + 3.2.0 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + + + + org.apache.shiro + shiro-spring-boot-web-starter + ${shiro.spring.version} + + + com.auth0 + java-jwt + ${jwt.auth0.version} + + + org.apache.httpcomponents + httpclient + 4.5.5 + + + org.apache.commons + commons-lang3 + 3.7 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + + org.apache.maven.plugins + maven-surefire-plugin + + true + + + + + \ No newline at end of file diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/ShiroWebApplication.java b/SpringBootShiroJWT/src/main/java/com/github/demo/ShiroWebApplication.java new file mode 100644 index 0000000..9ee3eea --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/ShiroWebApplication.java @@ -0,0 +1,12 @@ +package com.github.demo; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ShiroWebApplication { + + public static void main(String[] args) { + SpringApplication.run(ShiroWebApplication.class, args); + } +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/DbShiroRealm.java b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/DbShiroRealm.java new file mode 100644 index 0000000..8120685 --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/DbShiroRealm.java @@ -0,0 +1,67 @@ +package com.github.demo.configuration; + +import java.util.List; + +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.SimpleAuthenticationInfo; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.authc.credential.HashedCredentialsMatcher; +import org.apache.shiro.authz.AuthorizationInfo; +import org.apache.shiro.authz.SimpleAuthorizationInfo; +import org.apache.shiro.crypto.hash.Sha256Hash; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.subject.PrincipalCollection; +import org.apache.shiro.util.ByteSource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.github.demo.dto.UserDto; +import com.github.demo.service.UserService; + +public class DbShiroRealm extends AuthorizingRealm { + private final Logger log = LoggerFactory.getLogger(DbShiroRealm.class); + + private static final String encryptSalt = "F12839WhsnnEV$#23b"; + private UserService userService; + + public DbShiroRealm(UserService userService) { + this.userService = userService; + this.setCredentialsMatcher(new HashedCredentialsMatcher(Sha256Hash.ALGORITHM_NAME)); + } + + @Override + public boolean supports(AuthenticationToken token) { + return token instanceof UsernamePasswordToken; + } + // 登录验证 + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { + UsernamePasswordToken userpasswordToken = (UsernamePasswordToken)token; + String username = userpasswordToken.getUsername(); + UserDto user = userService.getUserInfo(username); + if(user == null) + throw new AuthenticationException("用户名或者密码错误"); + + return new SimpleAuthenticationInfo(user, user.getEncryptPwd(), ByteSource.Util.bytes(encryptSalt), "dbRealm"); + } + + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { + SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); + UserDto user = (UserDto) principals.getPrimaryPrincipal(); + List roles = user.getRoles(); + if(roles == null) { + roles = userService.getUserRoles(user.getUserId()); + user.setRoles(roles); + } + if (roles != null) + simpleAuthorizationInfo.addRoles(roles); + + return simpleAuthorizationInfo; + } + + +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/JWTCredentialsMatcher.java b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/JWTCredentialsMatcher.java new file mode 100644 index 0000000..ea517c9 --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/JWTCredentialsMatcher.java @@ -0,0 +1,42 @@ +package com.github.demo.configuration; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.JWTVerifier; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTVerificationException; +import com.github.demo.dto.UserDto; + +import org.apache.shiro.authc.AuthenticationInfo; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.authc.credential.CredentialsMatcher; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.UnsupportedEncodingException; + +public class JWTCredentialsMatcher implements CredentialsMatcher { + + private final Logger log = LoggerFactory.getLogger(JWTCredentialsMatcher.class); + + @Override + public boolean doCredentialsMatch(AuthenticationToken authenticationToken, AuthenticationInfo authenticationInfo) { + String token = (String) authenticationToken.getCredentials(); + Object stored = authenticationInfo.getCredentials(); + String salt = stored.toString(); + + UserDto user = (UserDto)authenticationInfo.getPrincipals().getPrimaryPrincipal(); + try { + Algorithm algorithm = Algorithm.HMAC256(salt); + JWTVerifier verifier = JWT.require(algorithm) + .withClaim("username", user.getUsername()) + .build(); + verifier.verify(token); + return true; + } catch (UnsupportedEncodingException | JWTVerificationException e) { + log.error("Token Error:{}", e.getMessage()); + } + + return false; + } + +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/JWTShiroRealm.java b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/JWTShiroRealm.java new file mode 100644 index 0000000..3c7efba --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/JWTShiroRealm.java @@ -0,0 +1,57 @@ +package com.github.demo.configuration; + +import org.apache.shiro.authc.*; +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 com.github.demo.dto.UserDto; +import com.github.demo.service.UserService; + + +/** + * 自定义身份认证 + * 基于HMAC( 散列消息认证码)的控制域 + */ + +public class JWTShiroRealm extends AuthorizingRealm { + private final Logger log = LoggerFactory.getLogger(JWTShiroRealm.class); + + protected UserService userService; + + public JWTShiroRealm(UserService userService){ + this.userService = userService; + this.setCredentialsMatcher(new JWTCredentialsMatcher()); + } + + @Override + public boolean supports(AuthenticationToken token) { + return token instanceof JWTToken; + } + + /** + * 认证信息.(身份验证) : Authentication 是用来验证用户身份 + * 默认使用此方法进行用户名正确与否验证,错误抛出异常即可。 + */ + @Override + protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { + JWTToken jwtToken = (JWTToken) authcToken; + String token = jwtToken.getToken(); + + UserDto user = userService.getJwtTokenInfo(JwtUtils.getUsername(token)); + if(user == null) + throw new AuthenticationException("token过期,请重新登录"); + + SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(user, user.getSalt(), "jwtRealm"); + + return authenticationInfo; + } + + @Override + protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { + return new SimpleAuthorizationInfo(); + } +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/JWTToken.java b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/JWTToken.java new file mode 100644 index 0000000..875b980 --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/JWTToken.java @@ -0,0 +1,42 @@ +package com.github.demo.configuration; + +import org.apache.shiro.authc.HostAuthenticationToken; + +public class JWTToken implements HostAuthenticationToken { + private static final long serialVersionUID = 9217639903967592166L; + + private String token; + private String host; + + public JWTToken(String token) { + this(token, null); + } + + public JWTToken(String token, String host) { + this.token = token; + this.host = host; + } + + public String getToken(){ + return this.token; + } + + public String getHost() { + return host; + } + + @Override + public Object getPrincipal() { + return token; + } + + @Override + public Object getCredentials() { + return token; + } + + @Override + public String toString(){ + return token + ':' + host; + } +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/JwtUtils.java b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/JwtUtils.java new file mode 100644 index 0000000..00fd3a2 --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/JwtUtils.java @@ -0,0 +1,82 @@ +package com.github.demo.configuration; + +import com.auth0.jwt.JWT; +import com.auth0.jwt.algorithms.Algorithm; +import com.auth0.jwt.exceptions.JWTDecodeException; +import com.auth0.jwt.interfaces.DecodedJWT; +import org.apache.shiro.crypto.SecureRandomNumberGenerator; + +import java.io.UnsupportedEncodingException; +import java.util.Calendar; +import java.util.Date; + +public class JwtUtils { + + /** + * 获得token中的信息无需secret解密也能获得 + * @return token中包含的签发时间 + */ + public static Date getIssuedAt(String token) { + try { + DecodedJWT jwt = JWT.decode(token); + return jwt.getIssuedAt(); + } catch (JWTDecodeException e) { + return null; + } + } + + /** + * 获得token中的信息无需secret解密也能获得 + * @return token中包含的用户名 + */ + public static String getUsername(String token) { + try { + DecodedJWT jwt = JWT.decode(token); + return jwt.getClaim("username").asString(); + } catch (JWTDecodeException e) { + return null; + } + } + + /** + * 生成签名,expireTime后过期 + * @param username 用户名 + * @param time 过期时间s + * @return 加密的token + */ + public static String sign(String username, String salt, long time) { + try { + Date date = new Date(System.currentTimeMillis()+time*1000); + Algorithm algorithm = Algorithm.HMAC256(salt); + // 附带username信息 + return JWT.create() + .withClaim("username", username) + .withExpiresAt(date) + .withIssuedAt(new Date()) + .sign(algorithm); + } catch (UnsupportedEncodingException e) { + return null; + } + } + + /** + * token是否过期 + * @return true:过期 + */ + public static boolean isTokenExpired(String token) { + Date now = Calendar.getInstance().getTime(); + DecodedJWT jwt = JWT.decode(token); + return jwt.getExpiresAt().before(now); + } + + /** + * 生成随机盐,长度32位 + * @return + */ + public static String generateSalt(){ + SecureRandomNumberGenerator secureRandom = new SecureRandomNumberGenerator(); + String hex = secureRandom.nextBytes(16).toHex(); + return hex; + } + +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/ResponseHeaderAdvice.java b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/ResponseHeaderAdvice.java new file mode 100644 index 0000000..989ece4 --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/ResponseHeaderAdvice.java @@ -0,0 +1,61 @@ +package com.github.demo.configuration; + +import org.springframework.core.MethodParameter; +import org.springframework.http.MediaType; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.server.ServerHttpRequest; +import org.springframework.http.server.ServerHttpResponse; +import org.springframework.http.server.ServletServerHttpRequest; +import org.springframework.http.server.ServletServerHttpResponse; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; + +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@ControllerAdvice +public class ResponseHeaderAdvice implements ResponseBodyAdvice { + @Override + public boolean supports(MethodParameter methodParameter, Class> aClass) { + return true; + } + + @Override + public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class> aClass, + ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) { + ServletServerHttpRequest serverRequest = (ServletServerHttpRequest)serverHttpRequest; + ServletServerHttpResponse serverResponse = (ServletServerHttpResponse)serverHttpResponse; + if(serverRequest == null || serverResponse == null + || serverRequest.getServletRequest() == null || serverResponse.getServletResponse() == null) { + return o; + } + + // 对于未添加跨域消息头的响应进行处理 + HttpServletRequest request = serverRequest.getServletRequest(); + HttpServletResponse response = serverResponse.getServletResponse(); + String originHeader = "Access-Control-Allow-Origin"; + if(!response.containsHeader(originHeader)) { + String origin = request.getHeader("Origin"); + if(origin == null) { + String referer = request.getHeader("Referer"); + if(referer != null) + origin = referer.substring(0, referer.indexOf("/", 7)); + } + response.setHeader("Access-Control-Allow-Origin", origin); + } + + String allowHeaders = "Access-Control-Allow-Headers"; + if(!response.containsHeader(allowHeaders)) + response.setHeader(allowHeaders, request.getHeader(allowHeaders)); + + String allowMethods = "Access-Control-Allow-Methods"; + if(!response.containsHeader(allowMethods)) + response.setHeader(allowMethods, "GET,POST,OPTIONS,HEAD"); + + String exposeHeaders = "access-control-expose-headers"; + if(!response.containsHeader(exposeHeaders)) + response.setHeader(exposeHeaders, "x-auth-token"); + + return o; + } +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/ShiroConfig.java b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/ShiroConfig.java new file mode 100644 index 0000000..0f665a6 --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/ShiroConfig.java @@ -0,0 +1,109 @@ +package com.github.demo.configuration; + +import org.apache.shiro.authc.Authenticator; +import org.apache.shiro.authc.pam.FirstSuccessfulStrategy; +import org.apache.shiro.authc.pam.ModularRealmAuthenticator; +import org.apache.shiro.mgt.SecurityManager; +import org.apache.shiro.mgt.SessionStorageEvaluator; +import org.apache.shiro.realm.Realm; +import org.apache.shiro.spring.web.ShiroFilterFactoryBean; +import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition; +import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition; +import org.apache.shiro.web.mgt.DefaultWebSessionStorageEvaluator; +import org.springframework.boot.web.servlet.FilterRegistrationBean; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import com.github.demo.filter.AnyRolesAuthorizationFilter; +import com.github.demo.filter.JwtAuthFilter; +import com.github.demo.service.UserService; + +import javax.servlet.DispatcherType; +import javax.servlet.Filter; + +import java.util.Arrays; +import java.util.Map; + +/** + * shiro配置类 + */ +@Configuration +public class ShiroConfig { + + @Bean + public FilterRegistrationBean filterRegistrationBean(SecurityManager securityManager,UserService userService) throws Exception{ + FilterRegistrationBean filterRegistration = new FilterRegistrationBean(); + filterRegistration.setFilter((Filter)shiroFilter(securityManager, userService).getObject()); + filterRegistration.addInitParameter("targetFilterLifecycle", "true"); + filterRegistration.setAsyncSupported(true); + filterRegistration.setEnabled(true); + filterRegistration.setDispatcherTypes(DispatcherType.REQUEST,DispatcherType.ASYNC); + + return filterRegistration; + } + + @Bean + public Authenticator authenticator(UserService userService) { + ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator(); + authenticator.setRealms(Arrays.asList(jwtShiroRealm(userService), dbShiroRealm(userService))); + authenticator.setAuthenticationStrategy(new FirstSuccessfulStrategy()); + return authenticator; + } + + @Bean + protected SessionStorageEvaluator sessionStorageEvaluator(){ + DefaultWebSessionStorageEvaluator sessionStorageEvaluator = new DefaultWebSessionStorageEvaluator(); + sessionStorageEvaluator.setSessionStorageEnabled(false); + return sessionStorageEvaluator; + } + + @Bean("dbRealm") + public Realm dbShiroRealm(UserService userService) { + DbShiroRealm myShiroRealm = new DbShiroRealm(userService); + return myShiroRealm; + } + + @Bean("jwtRealm") + public Realm jwtShiroRealm(UserService userService) { + JWTShiroRealm myShiroRealm = new JWTShiroRealm(userService); + return myShiroRealm; + } + + /** + * 设置过滤器 + */ + @Bean("shiroFilter") + public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, UserService userService) { + ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean(); + factoryBean.setSecurityManager(securityManager); + Map filterMap = factoryBean.getFilters(); + filterMap.put("authcToken", createAuthFilter(userService)); + filterMap.put("anyRole", createRolesFilter()); + factoryBean.setFilters(filterMap); + factoryBean.setFilterChainDefinitionMap(shiroFilterChainDefinition().getFilterChainMap()); + + return factoryBean; + } + + @Bean + protected ShiroFilterChainDefinition shiroFilterChainDefinition() { + DefaultShiroFilterChainDefinition chainDefinition = new DefaultShiroFilterChainDefinition(); + chainDefinition.addPathDefinition("/login", "noSessionCreation,anon"); + chainDefinition.addPathDefinition("/logout", "noSessionCreation,authcToken[permissive]"); + chainDefinition.addPathDefinition("/image/**", "anon"); + chainDefinition.addPathDefinition("/admin/**", "noSessionCreation,authcToken,anyRole[admin,manager]"); //只允许admin或manager角色的用户访问 + chainDefinition.addPathDefinition("/article/list", "noSessionCreation,authcToken"); + chainDefinition.addPathDefinition("/article/*", "noSessionCreation,authcToken[permissive]"); + chainDefinition.addPathDefinition("/**", "noSessionCreation,authcToken"); + return chainDefinition; + } + + protected JwtAuthFilter createAuthFilter(UserService userService){ + return new JwtAuthFilter(userService); + } + + protected AnyRolesAuthorizationFilter createRolesFilter(){ + return new AnyRolesAuthorizationFilter(); + } + +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/WebConfiguration.java b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/WebConfiguration.java new file mode 100644 index 0000000..e94c247 --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/configuration/WebConfiguration.java @@ -0,0 +1,25 @@ +package com.github.demo.configuration; + +import java.util.concurrent.Executors; + +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.concurrent.ConcurrentTaskExecutor; +import org.springframework.web.servlet.config.annotation.*; + +@Configuration +public class WebConfiguration extends WebMvcConfigurationSupport{ + + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**") + .allowedHeaders("*") + .allowedMethods("*") + .allowedOrigins("*"); + } + + @Override + protected void configureAsyncSupport(AsyncSupportConfigurer configurer) { + configurer.setTaskExecutor(new ConcurrentTaskExecutor(Executors.newFixedThreadPool(3))); + configurer.setDefaultTimeout(30000); + } +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/controller/ArticleController.java b/SpringBootShiroJWT/src/main/java/com/github/demo/controller/ArticleController.java new file mode 100644 index 0000000..7300a29 --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/controller/ArticleController.java @@ -0,0 +1,25 @@ +package com.github.demo.controller; + +import java.util.List; + +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import com.github.demo.dto.ArticleDto; + +@RestController +@RequestMapping("/article") +public class ArticleController { + + @GetMapping("/list") + public ResponseEntity> list(){ + return null; + } + + @GetMapping("/{id}") + public ResponseEntity read(@PathVariable Long id){ + return null; + } + + +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/controller/AsyncRequestController.java b/SpringBootShiroJWT/src/main/java/com/github/demo/controller/AsyncRequestController.java new file mode 100644 index 0000000..2ce705e --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/controller/AsyncRequestController.java @@ -0,0 +1,21 @@ +package com.github.demo.controller; + +import java.util.concurrent.Callable; + +import org.apache.shiro.SecurityUtils; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +import com.github.demo.dto.UserDto; + +@RestController +public class AsyncRequestController { + + @GetMapping("/async") + public Callable doAsync(){ + return ()->{ + Thread.sleep(5000); + return (UserDto)SecurityUtils.getSubject().getPrincipal(); + }; + } +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/controller/LoginController.java b/SpringBootShiroJWT/src/main/java/com/github/demo/controller/LoginController.java new file mode 100644 index 0000000..d3451c3 --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/controller/LoginController.java @@ -0,0 +1,68 @@ +package com.github.demo.controller; + +import com.github.demo.dto.UserDto; +import com.github.demo.service.UserService; + +import org.apache.shiro.SecurityUtils; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.UsernamePasswordToken; +import org.apache.shiro.subject.Subject; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.*; +import org.springframework.web.bind.annotation.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; + +@RestController +public class LoginController { + + private Logger logger = LoggerFactory.getLogger(LoginController.class); + + private UserService userService; + + public LoginController(UserService userService) { + this.userService = userService; + } + + /** + * 用户名密码登录 + * @param request + * @return token + */ + @PostMapping(value = "/login") + public ResponseEntity login(@RequestBody UserDto loginInfo, HttpServletRequest request, HttpServletResponse response){ + Subject subject = SecurityUtils.getSubject(); + try { + UsernamePasswordToken token = new UsernamePasswordToken(loginInfo.getUsername(), loginInfo.getPassword()); + subject.login(token); + + UserDto user = (UserDto) subject. getPrincipal(); + String newToken = userService.generateJwtToken(user.getUsername()); + response.setHeader("x-auth-token", newToken); + + return ResponseEntity.ok().build(); + } catch (AuthenticationException e) { + logger.error("User {} login fail, Reason:{}", loginInfo.getUsername(), e.getMessage()); + return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build(); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(); + } + } + + /** + * 退出登录 + * @return + */ + @GetMapping(value = "/logout") + public ResponseEntity logout() { + Subject subject = SecurityUtils.getSubject(); + if(subject.getPrincipals() != null) { + UserDto user = (UserDto)subject.getPrincipals().getPrimaryPrincipal(); + userService.deleteLoginInfo(user.getUsername()); + } + SecurityUtils.getSubject().logout(); + return ResponseEntity.ok().build(); + } + +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/dto/ArticleDto.java b/SpringBootShiroJWT/src/main/java/com/github/demo/dto/ArticleDto.java new file mode 100644 index 0000000..acb0e5a --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/dto/ArticleDto.java @@ -0,0 +1,108 @@ +package com.github.demo.dto; + +import java.util.Date; + +public class ArticleDto implements java.io.Serializable{ + private static final long serialVersionUID = -2440471074054288487L; + + private Long id; + private String title; + private String author; + private Date issueTime; + private Date created; + private Date modified; + private Long createUserId; + private String createUserName; + private Integer status; //0:待发布, 1:已发布, 2:已删除 + private String content; + private String headImgUrl; //标题图片 + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public String getAuthor() { + return author; + } + + public void setAuthor(String auth) { + this.author = auth; + } + + public Date getCreated() { + return created; + } + + public void setCreated(Date created) { + this.created = created; + } + + public Date getModified() { + return modified; + } + + public void setModified(Date modified) { + this.modified = modified; + } + + public Integer getStatus() { + return status; + } + + public void setStatus(Integer status) { + this.status = status; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public Date getIssueTime() { + return issueTime; + } + + public void setIssueTime(Date issueTime) { + this.issueTime = issueTime; + } + + public Long getCreateUserId() { + return createUserId; + } + + public void setCreateUserId(Long createUserId) { + this.createUserId = createUserId; + } + + public String getCreateUserName() { + return createUserName; + } + + public void setCreateUserName(String createUserName) { + this.createUserName = createUserName; + } + + public String getHeadImgUrl() { + return headImgUrl; + } + + public void setHeadImgUrl(String headImgUrl) { + this.headImgUrl = headImgUrl; + } + +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/dto/UserDto.java b/SpringBootShiroJWT/src/main/java/com/github/demo/dto/UserDto.java new file mode 100644 index 0000000..f93d7be --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/dto/UserDto.java @@ -0,0 +1,67 @@ +package com.github.demo.dto; + +import java.io.Serializable; +import java.util.List; + +/** + * 用户对象 + */ +public class UserDto implements Serializable { + private static final long serialVersionUID = -9077975168976887742L; + + private String username; + private char[] password; + private String encryptPwd; + private Long userId; + private String salt; + private List roles; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public Long getUserId() { + return userId; + } + + public void setUserId(Long userId) { + this.userId = userId; + } + + public char[] getPassword() { + return password; + } + + public void setPassword(char[] password) { + this.password = password; + } + + public String getSalt() { + return salt; + } + + public void setSalt(String salt) { + this.salt = salt; + } + + public List getRoles() { + return roles; + } + + public void setRoles(List roles) { + this.roles = roles; + } + + public String getEncryptPwd() { + return encryptPwd; + } + + public void setEncryptPwd(String encryptPwd) { + this.encryptPwd = encryptPwd; + } + +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/filter/AnyRolesAuthorizationFilter.java b/SpringBootShiroJWT/src/main/java/com/github/demo/filter/AnyRolesAuthorizationFilter.java new file mode 100644 index 0000000..82e8ed5 --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/filter/AnyRolesAuthorizationFilter.java @@ -0,0 +1,48 @@ +package com.github.demo.filter; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.http.HttpStatus; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.web.filter.authz.AuthorizationFilter; +import org.apache.shiro.web.util.WebUtils; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class AnyRolesAuthorizationFilter extends AuthorizationFilter { + + @Override + protected void postHandle(ServletRequest request, ServletResponse response){ + request.setAttribute("anyRolesAuthFilter.FILTERED", true); + } + + @Override + protected boolean isAccessAllowed(ServletRequest servletRequest, ServletResponse servletResponse, Object mappedValue) throws Exception { + Boolean afterFiltered = (Boolean)(servletRequest.getAttribute("anyRolesAuthFilter.FILTERED")); + if( BooleanUtils.isTrue(afterFiltered)) + return true; + + Subject subject = getSubject(servletRequest, servletResponse); + String[] rolesArray = (String[]) mappedValue; + if (rolesArray == null || rolesArray.length == 0) { //没有角色限制,有权限访问 + return true; + } + for (String role : rolesArray) { + if (subject.hasRole(role)) //若当前用户是rolesArray中的任何一个,则有权限访问 + return true; + } + return false; + } + + @Override + protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException { + HttpServletResponse httpResponse = WebUtils.toHttp(response); + httpResponse.setCharacterEncoding("UTF-8"); + httpResponse.setContentType("application/json;charset=utf-8"); + httpResponse.setStatus(HttpStatus.SC_UNAUTHORIZED); + return false; + } + +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/filter/JwtAuthFilter.java b/SpringBootShiroJWT/src/main/java/com/github/demo/filter/JwtAuthFilter.java new file mode 100644 index 0000000..dcb263c --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/filter/JwtAuthFilter.java @@ -0,0 +1,134 @@ +package com.github.demo.filter; + +import com.github.demo.configuration.JWTToken; +import com.github.demo.configuration.JwtUtils; +import com.github.demo.dto.UserDto; +import com.github.demo.service.UserService; + +import org.apache.commons.lang3.BooleanUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.http.HttpStatus; +import org.apache.shiro.authc.AuthenticationException; +import org.apache.shiro.authc.AuthenticationToken; +import org.apache.shiro.subject.Subject; +import org.apache.shiro.web.filter.authc.AuthenticatingFilter; +import org.apache.shiro.web.util.WebUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.context.request.async.WebAsyncManager; +import org.springframework.web.context.request.async.WebAsyncUtils; + +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.util.Date; + +public class JwtAuthFilter extends AuthenticatingFilter { + private final Logger log = LoggerFactory.getLogger(JwtAuthFilter.class); + + private static final int tokenRefreshInterval = 300; + private UserService userService; + + public JwtAuthFilter(UserService userService){ + this.userService = userService; + this.setLoginUrl("/login"); + } + + @Override + protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception { + HttpServletRequest httpServletRequest = WebUtils.toHttp(request); + if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) //对于OPTION请求做拦截,不做token校验 + return false; + + return super.preHandle(request, response); + } + + @Override + protected void postHandle(ServletRequest request, ServletResponse response){ + this.fillCorsHeader(WebUtils.toHttp(request), WebUtils.toHttp(response)); + request.setAttribute("jwtShiroFilter.FILTERED", true); + } + + @Override + protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) { + if(this.isLoginRequest(request, response)) + return true; + Boolean afterFiltered = (Boolean)(request.getAttribute("jwtShiroFilter.FILTERED")); + if( BooleanUtils.isTrue(afterFiltered)) + return true; + + boolean allowed = false; + try { + allowed = executeLogin(request, response); + } catch(IllegalStateException e){ //not found any token + log.error("Not found any token"); + }catch (Exception e) { + log.error("Error occurs when login", e); + } + return allowed || super.isPermissive(mappedValue); + } + + @Override + protected AuthenticationToken createToken(ServletRequest servletRequest, ServletResponse servletResponse) { + String jwtToken = getAuthzHeader(servletRequest); + if(StringUtils.isNotBlank(jwtToken)&&!JwtUtils.isTokenExpired(jwtToken)) + return new JWTToken(jwtToken); + + return null; + } + + @Override + protected boolean onAccessDenied(ServletRequest servletRequest, ServletResponse servletResponse) throws Exception { + HttpServletResponse httpResponse = WebUtils.toHttp(servletResponse); + httpResponse.setCharacterEncoding("UTF-8"); + httpResponse.setContentType("application/json;charset=UTF-8"); + httpResponse.setStatus(HttpStatus.SC_NON_AUTHORITATIVE_INFORMATION); + fillCorsHeader(WebUtils.toHttp(servletRequest), httpResponse); + return false; + } + + @Override + protected boolean onLoginSuccess(AuthenticationToken token, Subject subject, ServletRequest request, ServletResponse response) throws Exception { + HttpServletResponse httpResponse = WebUtils.toHttp(response); + String newToken = null; + if(token instanceof JWTToken){ + JWTToken jwtToken = (JWTToken)token; + UserDto user = (UserDto) subject.getPrincipal(); + boolean shouldRefresh = shouldTokenRefresh(JwtUtils.getIssuedAt(jwtToken.getToken())); + if(shouldRefresh) { + newToken = userService.generateJwtToken(user.getUsername()); + } + } + if(StringUtils.isNotBlank(newToken)) + httpResponse.setHeader("x-auth-token", newToken); + + return true; + } + + @Override + protected boolean onLoginFailure(AuthenticationToken token, AuthenticationException e, ServletRequest request, ServletResponse response) { + log.error("Validate token fail, token:{}, error:{}", token.toString(), e.getMessage()); + return false; + } + + protected String getAuthzHeader(ServletRequest request) { + HttpServletRequest httpRequest = WebUtils.toHttp(request); + String header = httpRequest.getHeader("x-auth-token"); + return StringUtils.removeStart(header, "Bearer "); + } + + protected boolean shouldTokenRefresh(Date issueAt){ + LocalDateTime issueTime = LocalDateTime.ofInstant(issueAt.toInstant(), ZoneId.systemDefault()); + return LocalDateTime.now().minusSeconds(tokenRefreshInterval).isAfter(issueTime); + } + + protected void fillCorsHeader(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse){ + httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin")); + httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,HEAD"); + httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers")); + } +} diff --git a/SpringBootShiroJWT/src/main/java/com/github/demo/service/UserService.java b/SpringBootShiroJWT/src/main/java/com/github/demo/service/UserService.java new file mode 100644 index 0000000..430bf1a --- /dev/null +++ b/SpringBootShiroJWT/src/main/java/com/github/demo/service/UserService.java @@ -0,0 +1,90 @@ +package com.github.demo.service; + +import java.util.Arrays; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import org.apache.shiro.crypto.hash.Sha256Hash; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.StringRedisTemplate; +import org.springframework.stereotype.Service; + +import com.github.demo.configuration.JwtUtils; +import com.github.demo.dto.UserDto; + +/** + * 用户信息接口 + */ +@Service +public class UserService { + + private static final String encryptSalt = "F12839WhsnnEV$#23b"; + + @Autowired + private StringRedisTemplate redisTemplate; + + /** + * 保存user登录信息,返回token + * @param userDto + */ + public String generateJwtToken(String username) { + String salt = "12345";//JwtUtils.generateSalt(); + /** + * @todo 将salt保存到数据库或者缓存中 + * redisTemplate.opsForValue().set("token:"+username, salt, 3600, TimeUnit.SECONDS); + */ + return JwtUtils.sign(username, salt, 3600); //生成jwt token,设置过期时间为1小时 + } + + /** + * 获取上次token生成时的salt值和登录用户信息 + * @param username + * @return + */ + public UserDto getJwtTokenInfo(String username) { + String salt = "12345"; + /** + * @todo 从数据库或者缓存中取出jwt token生成时用的salt + * salt = redisTemplate.opsForValue().get("token:"+username); + */ + UserDto user = getUserInfo(username); + user.setSalt(salt); + return user; + } + + /** + * 清除token信息 + * @param userName 登录用户名 + * @param terminal 登录终端 + */ + public void deleteLoginInfo(String username) { + /** + * @todo 删除数据库或者缓存中保存的salt + * redisTemplate.delete("token:"+username); + */ + + } + + /** + * 获取数据库中保存的用户信息,主要是加密后的密码 + * @param userName + * @return + */ + public UserDto getUserInfo(String userName) { + UserDto user = new UserDto(); + user.setUserId(1L); + user.setUsername("admin"); + user.setEncryptPwd(new Sha256Hash("123456", encryptSalt).toHex()); + return user; + } + + /** + * 获取用户角色列表,强烈建议从缓存中获取 + * @param userId + * @return + */ + public List getUserRoles(Long userId){ + return Arrays.asList("admin"); + } + +} diff --git a/pom.xml b/pom.xml index d36009e..ee2280b 100644 --- a/pom.xml +++ b/pom.xml @@ -25,6 +25,7 @@ SpringBootShiroAuthorization SpringBootException SpringBootMybatis + SpringBootShiroJWT