From 59a5b1ad9ca552bde6721fae02f54f8079ff16c0 Mon Sep 17 00:00:00 2001 From: macro Date: Sat, 9 Nov 2019 16:32:27 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0mall-security=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- mall-security/.gitignore | 31 ++++ mall-security/pom.xml | 38 +++++ .../JwtAuthenticationTokenFilter.java | 57 +++++++ .../RestAuthenticationEntryPoint.java | 26 ++++ .../component/RestfulAccessDeniedHandler.java | 28 ++++ .../security/config/IgnoreUrlsConfig.java | 22 +++ .../mall/security/config/SecurityConfig.java | 99 ++++++++++++ .../mall/security/util/JwtTokenUtil.java | 144 ++++++++++++++++++ 8 files changed, 445 insertions(+) create mode 100644 mall-security/.gitignore create mode 100644 mall-security/pom.xml create mode 100644 mall-security/src/main/java/com/macro/mall/security/component/JwtAuthenticationTokenFilter.java create mode 100644 mall-security/src/main/java/com/macro/mall/security/component/RestAuthenticationEntryPoint.java create mode 100644 mall-security/src/main/java/com/macro/mall/security/component/RestfulAccessDeniedHandler.java create mode 100644 mall-security/src/main/java/com/macro/mall/security/config/IgnoreUrlsConfig.java create mode 100644 mall-security/src/main/java/com/macro/mall/security/config/SecurityConfig.java create mode 100644 mall-security/src/main/java/com/macro/mall/security/util/JwtTokenUtil.java diff --git a/mall-security/.gitignore b/mall-security/.gitignore new file mode 100644 index 0000000..a2a3040 --- /dev/null +++ b/mall-security/.gitignore @@ -0,0 +1,31 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ + +### VS Code ### +.vscode/ diff --git a/mall-security/pom.xml b/mall-security/pom.xml new file mode 100644 index 0000000..1ed891e --- /dev/null +++ b/mall-security/pom.xml @@ -0,0 +1,38 @@ + + + 4.0.0 + com.macro.mall + mall-security + 1.0-SNAPSHOT + jar + + mall-security + mall-security project for mall + + + com.macro.mall + mall + 1.0-SNAPSHOT + + + + + com.macro.mall + mall-common + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + io.jsonwebtoken + jjwt + + + + diff --git a/mall-security/src/main/java/com/macro/mall/security/component/JwtAuthenticationTokenFilter.java b/mall-security/src/main/java/com/macro/mall/security/component/JwtAuthenticationTokenFilter.java new file mode 100644 index 0000000..bddd916 --- /dev/null +++ b/mall-security/src/main/java/com/macro/mall/security/component/JwtAuthenticationTokenFilter.java @@ -0,0 +1,57 @@ +package com.macro.mall.security.component; + +import com.macro.mall.security.util.JwtTokenUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.web.authentication.WebAuthenticationDetailsSource; +import org.springframework.web.filter.OncePerRequestFilter; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * JWT登录授权过滤器 + * Created by macro on 2018/4/26. + */ +public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { + private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class); + @Autowired + private UserDetailsService userDetailsService; + @Autowired + private JwtTokenUtil jwtTokenUtil; + @Value("${jwt.tokenHeader}") + private String tokenHeader; + @Value("${jwt.tokenHead}") + private String tokenHead; + + @Override + protected void doFilterInternal(HttpServletRequest request, + HttpServletResponse response, + FilterChain chain) throws ServletException, IOException { + String authHeader = request.getHeader(this.tokenHeader); + if (authHeader != null && authHeader.startsWith(this.tokenHead)) { + String authToken = authHeader.substring(this.tokenHead.length());// The part after "Bearer " + String username = jwtTokenUtil.getUserNameFromToken(authToken); + LOGGER.info("checking username:{}", username); + if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { + UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); + if (jwtTokenUtil.validateToken(authToken, userDetails)) { + UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); + authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); + LOGGER.info("authenticated user:{}", username); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + } + } + chain.doFilter(request, response); + } +} diff --git a/mall-security/src/main/java/com/macro/mall/security/component/RestAuthenticationEntryPoint.java b/mall-security/src/main/java/com/macro/mall/security/component/RestAuthenticationEntryPoint.java new file mode 100644 index 0000000..a0b370c --- /dev/null +++ b/mall-security/src/main/java/com/macro/mall/security/component/RestAuthenticationEntryPoint.java @@ -0,0 +1,26 @@ +package com.macro.mall.security.component; + +import cn.hutool.json.JSONUtil; +import com.macro.mall.common.api.CommonResult; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 当未登录或者token失效访问接口时,自定义的返回结果 + * Created by macro on 2018/5/14. + */ +public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json"); + response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(authException.getMessage()))); + response.getWriter().flush(); + } +} diff --git a/mall-security/src/main/java/com/macro/mall/security/component/RestfulAccessDeniedHandler.java b/mall-security/src/main/java/com/macro/mall/security/component/RestfulAccessDeniedHandler.java new file mode 100644 index 0000000..b885449 --- /dev/null +++ b/mall-security/src/main/java/com/macro/mall/security/component/RestfulAccessDeniedHandler.java @@ -0,0 +1,28 @@ +package com.macro.mall.security.component; + +import cn.hutool.json.JSONUtil; +import com.macro.mall.common.api.CommonResult; +import org.springframework.security.access.AccessDeniedException; +import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 当访问接口没有权限时,自定义的返回结果 + * Created by macro on 2018/4/26. + */ +public class RestfulAccessDeniedHandler implements AccessDeniedHandler{ + @Override + public void handle(HttpServletRequest request, + HttpServletResponse response, + AccessDeniedException e) throws IOException, ServletException { + response.setCharacterEncoding("UTF-8"); + response.setContentType("application/json"); + response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage()))); + response.getWriter().flush(); + } +} diff --git a/mall-security/src/main/java/com/macro/mall/security/config/IgnoreUrlsConfig.java b/mall-security/src/main/java/com/macro/mall/security/config/IgnoreUrlsConfig.java new file mode 100644 index 0000000..0dc852a --- /dev/null +++ b/mall-security/src/main/java/com/macro/mall/security/config/IgnoreUrlsConfig.java @@ -0,0 +1,22 @@ +package com.macro.mall.security.config; + +import lombok.Data; +import lombok.Getter; +import lombok.Setter; +import org.springframework.boot.context.properties.ConfigurationProperties; + +import java.util.ArrayList; +import java.util.List; + +/** + * 用于配置不需要保护的资源路径 + * Created by macro on 2018/11/5. + */ +@Getter +@Setter +@ConfigurationProperties(prefix = "ignored") +public class IgnoreUrlsConfig { + + private List urls = new ArrayList<>(); + +} diff --git a/mall-security/src/main/java/com/macro/mall/security/config/SecurityConfig.java b/mall-security/src/main/java/com/macro/mall/security/config/SecurityConfig.java new file mode 100644 index 0000000..6fea681 --- /dev/null +++ b/mall-security/src/main/java/com/macro/mall/security/config/SecurityConfig.java @@ -0,0 +1,99 @@ +package com.macro.mall.security.config; + +import com.macro.mall.security.component.JwtAuthenticationTokenFilter; +import com.macro.mall.security.component.RestAuthenticationEntryPoint; +import com.macro.mall.security.component.RestfulAccessDeniedHandler; +import com.macro.mall.security.util.JwtTokenUtil; +import org.springframework.context.annotation.Bean; +import org.springframework.http.HttpMethod; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; +import org.springframework.security.config.http.SessionCreationPolicy; +import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; +import org.springframework.security.crypto.password.PasswordEncoder; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; + + +/** + * 对SpringSecurity的配置的扩展,支持自定义白名单资源路径和查询用户逻辑 + * Created by macro on 2019/11/5. + */ +public class SecurityConfig extends WebSecurityConfigurerAdapter { + + @Override + protected void configure(HttpSecurity httpSecurity) throws Exception { + ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry registry = httpSecurity + .authorizeRequests(); + for (String url : ignoreUrlsConfig().getUrls()) { + registry.antMatchers(url).permitAll(); + } + //允许跨域请求的OPTIONS请求 + registry.antMatchers(HttpMethod.OPTIONS) + .permitAll(); + // 任何请求需要身份认证 + registry.and() + .authorizeRequests() + .anyRequest() + .authenticated() + // 关闭跨站请求防护及不使用session + .and() + .csrf() + .disable() + .sessionManagement() + .sessionCreationPolicy(SessionCreationPolicy.STATELESS) + // 自定义权限拒绝处理类 + .and() + .exceptionHandling() + .accessDeniedHandler(restfulAccessDeniedHandler()) + .authenticationEntryPoint(restAuthenticationEntryPoint()) + // 自定义权限拦截器JWT过滤器 + .and() + .addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); + } + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth.userDetailsService(userDetailsService()) + .passwordEncoder(passwordEncoder()); + } + + @Bean + public PasswordEncoder passwordEncoder() { + return new BCryptPasswordEncoder(); + } + + @Bean + public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter() { + return new JwtAuthenticationTokenFilter(); + } + + @Bean + @Override + public AuthenticationManager authenticationManagerBean() throws Exception { + return super.authenticationManagerBean(); + } + + @Bean + public RestfulAccessDeniedHandler restfulAccessDeniedHandler() { + return new RestfulAccessDeniedHandler(); + } + + @Bean + public RestAuthenticationEntryPoint restAuthenticationEntryPoint() { + return new RestAuthenticationEntryPoint(); + } + + @Bean + public IgnoreUrlsConfig ignoreUrlsConfig() { + return new IgnoreUrlsConfig(); + } + + @Bean + public JwtTokenUtil jwtTokenUtil() { + return new JwtTokenUtil(); + } + +} diff --git a/mall-security/src/main/java/com/macro/mall/security/util/JwtTokenUtil.java b/mall-security/src/main/java/com/macro/mall/security/util/JwtTokenUtil.java new file mode 100644 index 0000000..3c4338d --- /dev/null +++ b/mall-security/src/main/java/com/macro/mall/security/util/JwtTokenUtil.java @@ -0,0 +1,144 @@ +package com.macro.mall.security.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.userdetails.UserDetails; + +import java.util.Date; +import java.util.HashMap; +import java.util.Map; + +/** + * JwtToken生成的工具类 + * JWT token的格式:header.payload.signature + * header的格式(算法、token的类型): + * {"alg": "HS512","typ": "JWT"} + * payload的格式(用户名、创建时间、生成时间): + * {"sub":"wang","created":1489079981393,"exp":1489684781} + * signature的生成算法: + * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) + * Created by macro on 2018/4/26. + */ +public class JwtTokenUtil { + private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class); + private static final String CLAIM_KEY_USERNAME = "sub"; + private static final String CLAIM_KEY_CREATED = "created"; + @Value("${jwt.secret}") + private String secret; + @Value("${jwt.expiration}") + private Long expiration; + @Value("${jwt.tokenHead}") + private String tokenHead; + + /** + * 根据负责生成JWT的token + */ + private String generateToken(Map claims) { + return Jwts.builder() + .setClaims(claims) + .setExpiration(generateExpirationDate()) + .signWith(SignatureAlgorithm.HS512, secret) + .compact(); + } + + /** + * 从token中获取JWT中的负载 + */ + private Claims getClaimsFromToken(String token) { + Claims claims = null; + try { + claims = Jwts.parser() + .setSigningKey(secret) + .parseClaimsJws(token) + .getBody(); + } catch (Exception e) { + LOGGER.info("JWT格式验证失败:{}", token); + } + return claims; + } + + /** + * 生成token的过期时间 + */ + private Date generateExpirationDate() { + return new Date(System.currentTimeMillis() + expiration * 1000); + } + + /** + * 从token中获取登录用户名 + */ + public String getUserNameFromToken(String token) { + String username; + try { + Claims claims = getClaimsFromToken(token); + username = claims.getSubject(); + } catch (Exception e) { + username = null; + } + return username; + } + + /** + * 验证token是否还有效 + * + * @param token 客户端传入的token + * @param userDetails 从数据库中查询出来的用户信息 + */ + public boolean validateToken(String token, UserDetails userDetails) { + String username = getUserNameFromToken(token); + return username.equals(userDetails.getUsername()) && !isTokenExpired(token); + } + + /** + * 判断token是否已经失效 + */ + private boolean isTokenExpired(String token) { + Date expiredDate = getExpiredDateFromToken(token); + return expiredDate.before(new Date()); + } + + /** + * 从token中获取过期时间 + */ + private Date getExpiredDateFromToken(String token) { + Claims claims = getClaimsFromToken(token); + return claims.getExpiration(); + } + + /** + * 根据用户信息生成token + */ + public String generateToken(UserDetails userDetails) { + Map claims = new HashMap<>(); + claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } + + /** + * 判断token是否可以被刷新 + */ + private boolean canRefresh(String token) { + return !isTokenExpired(token); + } + + + /** + * 当原来的token没过期是可以刷新 + * + * @param oldToken 带tokenHead的token + */ + public String refreshHeadToken(String oldToken) { + String token = oldToken.substring(tokenHead.length()); + if (canRefresh(token)) { + Claims claims = getClaimsFromToken(token); + claims.put(CLAIM_KEY_CREATED, new Date()); + return generateToken(claims); + } + return null; + } +}