diff --git a/build.gradle b/build.gradle index 2260eaa..0d028c9 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,7 @@ dependencies { implementation 'com.aiprose:jpa-common-utils:2.3.4' implementation 'org.apache.commons:commons-lang3:3.10' + implementation 'com.alibaba:fastjson:1.2.47' } test { diff --git a/src/main/java/com/aiprose/scauth/conf/WebSecurityConfig.java b/src/main/java/com/aiprose/scauth/conf/WebSecurityConfig.java index 821044e..ed23b83 100644 --- a/src/main/java/com/aiprose/scauth/conf/WebSecurityConfig.java +++ b/src/main/java/com/aiprose/scauth/conf/WebSecurityConfig.java @@ -1,10 +1,12 @@ package com.aiprose.scauth.conf; import com.aiprose.scauth.entity.User; +import com.aiprose.scauth.filter.JWTAuthenticationFilter; import com.aiprose.scauth.handler.*; import com.aiprose.scauth.service.IUserService; import org.apache.commons.lang3.StringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.access.AccessDecisionManager; import org.springframework.security.access.AccessDecisionVoter; @@ -17,11 +19,15 @@ import org.springframework.security.config.annotation.method.configuration.Enabl import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.http.SessionCreationPolicy; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.access.expression.WebExpressionVoter; import org.springframework.security.web.access.intercept.FilterSecurityInterceptor; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import java.util.ArrayList; import java.util.List; @@ -34,32 +40,34 @@ import java.util.List; * @since 1.0 */ @Configuration -@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true) +//@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true, jsr250Enabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private IUserService userService; @Autowired private RemeberMeHandler remeberMeHandler; - + @Override public void configure(WebSecurity web) throws Exception { // super.configure(web); + web.ignoring().antMatchers("login", "/v2/api-docs/**", "/swagger-resources/**", "/swagger-ui.html"); } @Override protected void configure(HttpSecurity http) throws Exception { - http.csrf().disable(); + http.cors().and().csrf().disable(); - // 配置记住我的参数和记住我处理类 - http.rememberMe() - .tokenRepository(remeberMeHandler) - .tokenValiditySeconds(60*60*24) - .userDetailsService(userDetailsService()); + // 授权配置 + http.authorizeRequests().anyRequest().authenticated(); - // 配置登录页面 - http.formLogin().loginPage("/login").permitAll(); + // 配置登录 + http.formLogin().usernameParameter("username").passwordParameter("password").loginProcessingUrl("/login"); + //登录过期、 未登录 + http.exceptionHandling().authenticationEntryPoint(new LoginExpireHandler()); + // 配置登录失败后的操作 + http.formLogin().failureHandler(new LoginFailureHandler()); // 配置登录成功后的操作 http.formLogin().successHandler(new LoginSuccessHandler()); @@ -67,43 +75,37 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { http.exceptionHandling().accessDeniedHandler(new AuthLimitHandler()); // 登出授权 - http.logout().permitAll(); +// http.logout().permitAll(); - // 授权配置 - http.authorizeRequests() - /* 所有静态文件可以访问 */ - .antMatchers("/js/**","/css/**","/images/**").permitAll() - /* 所有 以/ad 开头的 广告页面可以访问 */ - .antMatchers("/ad/**").permitAll() - .antMatchers("/user/**","/role/**").permitAll() - /* 动态url权限 */ - .withObjectPostProcessor(new DefinedObjectPostProcessor()) - /* url决策 */ - .accessDecisionManager(accessDecisionManager()) - .anyRequest().authenticated(); + http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); + + /* 配置token验证过滤器 */ + http.addFilterBefore(new JWTAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()).passwordEncoder(new BCryptPasswordEncoder()); -// auth.passwordEncoder(new BCryptPasswordEncoder()); -// .withUser("nelson").password(new BCryptPasswordEncoder().encode("123456")).roles("admin") -// .and() -// .withUser("yasaka").password(new BCryptPasswordEncoder().encode("123456")).roles("user") -// .and() -// .withUser("one").password(new BCryptPasswordEncoder().encode("123456")).roles("gest") -// .and() -// .withUser("admin").password(new BCryptPasswordEncoder().encode("123456")).roles("root"); + } + + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry.addMapping("/**"); + } + }; } @Override protected UserDetailsService userDetailsService() { return username -> { - if(StringUtils.isBlank(username)){ + if (StringUtils.isBlank(username)) { throw new UsernameNotFoundException("用户名为空"); } User user = userService.findByUsernameAndRole(username); - if(user == null){ + if (user == null) { throw new UsernameNotFoundException("用户不存在"); } return user; @@ -117,25 +119,25 @@ public class WebSecurityConfig extends WebSecurityConfigurerAdapter { * * 决策管理 */ - private AccessDecisionManager accessDecisionManager() { - List> decisionVoters = new ArrayList<>(); - decisionVoters.add(new WebExpressionVoter()); - decisionVoters.add(new AuthenticatedVoter()); - decisionVoters.add(new RoleVoter()); - /* 路由权限管理 */ - decisionVoters.add(new UrlRoleAuthHandler()); - return new UnanimousBased(decisionVoters); - } - - @Autowired - private UrlRolesFilterHandler urlRolesFilterHandler; - - - class DefinedObjectPostProcessor implements ObjectPostProcessor { - @Override - public O postProcess(O object) { - object.setSecurityMetadataSource(urlRolesFilterHandler); - return object; - } - } +// private AccessDecisionManager accessDecisionManager() { +// List> decisionVoters = new ArrayList<>(); +// decisionVoters.add(new WebExpressionVoter()); +// decisionVoters.add(new AuthenticatedVoter()); +// decisionVoters.add(new RoleVoter()); +// /* 路由权限管理 */ +// decisionVoters.add(new UrlRoleAuthHandler()); +// return new UnanimousBased(decisionVoters); +// } + +// @Autowired +// private UrlRolesFilterHandler urlRolesFilterHandler; + + +// class DefinedObjectPostProcessor implements ObjectPostProcessor { +// @Override +// public O postProcess(O object) { +// object.setSecurityMetadataSource(urlRolesFilterHandler); +// return object; +// } +// } } diff --git a/src/main/java/com/aiprose/scauth/controller/LoginController.java b/src/main/java/com/aiprose/scauth/controller/LoginController.java index b92c314..fafbe7f 100644 --- a/src/main/java/com/aiprose/scauth/controller/LoginController.java +++ b/src/main/java/com/aiprose/scauth/controller/LoginController.java @@ -5,9 +5,8 @@ import org.springframework.web.bind.annotation.GetMapping; @Controller public class LoginController { - - @GetMapping("/login") - public String login(){ - return "login"; - } +// @GetMapping("/login") +// public String login(){ +// return "login"; +// } } \ No newline at end of file diff --git a/src/main/java/com/aiprose/scauth/entity/Jwt.java b/src/main/java/com/aiprose/scauth/entity/Jwt.java new file mode 100644 index 0000000..d30ed04 --- /dev/null +++ b/src/main/java/com/aiprose/scauth/entity/Jwt.java @@ -0,0 +1,26 @@ +package com.aiprose.scauth.entity; + +import com.aiprose.scauth.util.JwtUtils; +import lombok.Data; + +@Data +public class Jwt { + /* 头部 */ + private String header; + /* 负载 */ + private String payload; + /* 签名 */ + private String signature; + + public Jwt(String payload) throws Exception { + this.header = JwtUtils.encode(JwtUtils.DEFAULT_HEADER); + this.payload = JwtUtils.encode(payload); + this.signature = JwtUtils.getSignature(payload); + } + + /* jwt最终结果 */ + @Override + public String toString() { + return header + "." + payload + "." + signature; + } +} diff --git a/src/main/java/com/aiprose/scauth/entity/Menu.java b/src/main/java/com/aiprose/scauth/entity/Menu.java index 8e90927..ad2b397 100644 --- a/src/main/java/com/aiprose/scauth/entity/Menu.java +++ b/src/main/java/com/aiprose/scauth/entity/Menu.java @@ -13,7 +13,7 @@ import javax.persistence.Table; * @date 2020/11/27 17:04 * @since 1.0 */ -@Data + @Data @Entity @Table(name = "sys_menu") public class Menu extends IDEntity { diff --git a/src/main/java/com/aiprose/scauth/entity/Role.java b/src/main/java/com/aiprose/scauth/entity/Role.java index 7f24737..b287e7a 100644 --- a/src/main/java/com/aiprose/scauth/entity/Role.java +++ b/src/main/java/com/aiprose/scauth/entity/Role.java @@ -1,6 +1,7 @@ package com.aiprose.scauth.entity; import lombok.Data; +import lombok.EqualsAndHashCode; import org.springframework.security.core.GrantedAuthority; import javax.persistence.Entity; @@ -15,6 +16,7 @@ import javax.persistence.Table; */ @Data @Entity +@EqualsAndHashCode(callSuper=false) @Table(name="sys_role") public class Role extends IDEntity implements GrantedAuthority { private String role; diff --git a/src/main/java/com/aiprose/scauth/entity/User.java b/src/main/java/com/aiprose/scauth/entity/User.java index d37995a..aa21488 100644 --- a/src/main/java/com/aiprose/scauth/entity/User.java +++ b/src/main/java/com/aiprose/scauth/entity/User.java @@ -26,6 +26,19 @@ public class User extends IDEntity implements UserDetails { private String username; private String password; + /* 指示是否未过期的用户的凭据(密码),过期的凭据防止认证 默认true 默认表示未过期 */ + @Transient + private boolean credentialsNonExpired = true; + + //账户是否未过期,过期无法验证 默认true表示未过期 + private boolean accountNonExpired = true; + + //用户是未被锁定,锁定的用户无法进行身份验证 默认true表示未锁定 + private boolean accountNonLocked = true; + + //是否可用 ,禁用的用户不能身份验证 默认true表示可用 + private boolean enabled = true; + @Transient private List roles; diff --git a/src/main/java/com/aiprose/scauth/entity/UserRole.java b/src/main/java/com/aiprose/scauth/entity/UserRole.java index 4303ab2..06771a9 100644 --- a/src/main/java/com/aiprose/scauth/entity/UserRole.java +++ b/src/main/java/com/aiprose/scauth/entity/UserRole.java @@ -1,6 +1,7 @@ package com.aiprose.scauth.entity; import lombok.Data; +import lombok.EqualsAndHashCode; import javax.persistence.Entity; import javax.persistence.Table; @@ -14,6 +15,7 @@ import javax.persistence.Table; */ @Data @Entity +@EqualsAndHashCode(callSuper=false) @Table(name="sys_user_role") public class UserRole extends IDEntity{ private Integer sysUserId; diff --git a/src/main/java/com/aiprose/scauth/filter/JWTAuthenticationFilter.java b/src/main/java/com/aiprose/scauth/filter/JWTAuthenticationFilter.java new file mode 100644 index 0000000..5fcecee --- /dev/null +++ b/src/main/java/com/aiprose/scauth/filter/JWTAuthenticationFilter.java @@ -0,0 +1,48 @@ +package com.aiprose.scauth.filter; + +import com.aiprose.scauth.entity.User; +import com.aiprose.scauth.util.JwtUtils; +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.JSONObject; +import org.springframework.http.MediaType; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.filter.GenericFilterBean; + +import javax.servlet.FilterChain; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +public class JWTAuthenticationFilter extends GenericFilterBean { + + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { + HttpServletRequest req = (HttpServletRequest) request; + String token = req.getHeader(JwtUtils.HEADER_TOKEN_NAME); + /* token为null直接走登录的过滤器,不为空走下面 */ + if (token!=null&&token.trim().length()>0) { + String tokenBody = null; + try { + tokenBody = JwtUtils.testJwt(token); + } catch (Exception e) { + e.printStackTrace(); + } + /* 从token中取出用户信息,放在上下文中 */ + if (tokenBody!=null&&tokenBody.trim().length()>0){ + JSONObject user = JSON.parseObject(tokenBody).getJSONObject("user"); + User sysUser = JSON.toJavaObject(user, User.class); + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(sysUser,null,sysUser.getAuthorities())); + }else{ + HttpServletResponse res = (HttpServletResponse) response; + res.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + res.getWriter().write("{\"code\": \"405\", \"msg\": \"token错误\"}"); + return; + } + } + filterChain.doFilter(request, response); + } +} \ No newline at end of file diff --git a/src/main/java/com/aiprose/scauth/handler/LoginExpireHandler.java b/src/main/java/com/aiprose/scauth/handler/LoginExpireHandler.java new file mode 100644 index 0000000..d96bd67 --- /dev/null +++ b/src/main/java/com/aiprose/scauth/handler/LoginExpireHandler.java @@ -0,0 +1,28 @@ +package com.aiprose.scauth.handler; + +import org.springframework.http.MediaType; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author nelson + * @desc TODO + * @company 北京中经网软件有限公司 + * @date 2020/11/27 16:15 + * @since 1.0 + */ +public class LoginExpireHandler implements AuthenticationEntryPoint { + + @Override + public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { + authException.printStackTrace(); + response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + response.getWriter().write("{\"code\": \"401\", \"msg\": \"登录过期或未登录\"}"); + } +} diff --git a/src/main/java/com/aiprose/scauth/handler/LoginFailureHandler.java b/src/main/java/com/aiprose/scauth/handler/LoginFailureHandler.java new file mode 100644 index 0000000..dbe15f4 --- /dev/null +++ b/src/main/java/com/aiprose/scauth/handler/LoginFailureHandler.java @@ -0,0 +1,28 @@ +package com.aiprose.scauth.handler; + +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.authentication.AuthenticationFailureHandler; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * @author nelson + * @desc TODO + * @company 北京中经网软件有限公司 + * @date 2020/11/27 16:15 + * @since 1.0 + */ +public class LoginFailureHandler implements AuthenticationFailureHandler { + + @Override + public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { + response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + response.getWriter().write("{\"code\": \"500\", \"msg\": \"登录失败\"}"); + } +} diff --git a/src/main/java/com/aiprose/scauth/handler/LoginSuccessHandler.java b/src/main/java/com/aiprose/scauth/handler/LoginSuccessHandler.java index 13a9019..00b690b 100644 --- a/src/main/java/com/aiprose/scauth/handler/LoginSuccessHandler.java +++ b/src/main/java/com/aiprose/scauth/handler/LoginSuccessHandler.java @@ -1,5 +1,10 @@ package com.aiprose.scauth.handler; +import com.aiprose.scauth.entity.Jwt; +import com.aiprose.scauth.entity.User; +import com.aiprose.scauth.util.JwtUtils; +import com.alibaba.fastjson.JSONObject; +import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @@ -18,11 +23,32 @@ import java.io.IOException; public class LoginSuccessHandler implements AuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { - System.out.println("login success"); - System.out.println(authentication.getDetails()); - System.out.println(authentication.getAuthorities()); - System.out.println(authentication.getCredentials()); - System.out.println(authentication.getPrincipal()); - response.sendRedirect("/home"); + // 获取登录成功信息 + response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE); + boolean loginBoolean = true; + User user = (User) authentication.getPrincipal(); + user.setPassword(null); + long now = System.currentTimeMillis(); + JSONObject payload = new JSONObject(); + payload.put("iss", "sys"); //签发人 + payload.put("aud", user.getUsername()); //受众 + payload.put("exp", now + JwtUtils.EXPIRE_TIME); //过期时间 + payload.put("nbf", now); //生效时间 + payload.put("iat", now); //签发时间 + payload.put("jti", user.getId()); //编号 + payload.put("sub", "JWT-TEST"); //主题 + payload.put("user", user); //用户对象 + + try { + String token = new Jwt(payload.toJSONString()).toString(); + response.setHeader(JwtUtils.HEADER_TOKEN_NAME, token); + if (loginBoolean) { + response.getWriter().write("{\"code\": \"200\", \"msg\": \"登录成功\", \"token\": \"" + token + "\"}"); + } else { + response.getWriter().write("{\"code\": \"500\", \"msg\": \"登录失败\"}"); + } + } catch (Exception e) { + loginBoolean = false; + } } } diff --git a/src/main/java/com/aiprose/scauth/util/JwtUtils.java b/src/main/java/com/aiprose/scauth/util/JwtUtils.java new file mode 100644 index 0000000..292f820 --- /dev/null +++ b/src/main/java/com/aiprose/scauth/util/JwtUtils.java @@ -0,0 +1,80 @@ +package com.aiprose.scauth.util; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; +import java.util.Base64; + +public class JwtUtils { + + /* 默认head */ + public static final String DEFAULT_HEADER = "{\"alg\": \"HS256\",\"typ\": \"JWT\"}"; + + /* HmacSHA256 加密算法 秘钥 */ + public static final String SECRET = "12345"; + + /* token有效时间 1天 */ + public static final long EXPIRE_TIME = 1000 * 60 * 60 * 24; + + /* token在header中的名字 */ + public static final String HEADER_TOKEN_NAME = "Authorization"; + + /** + * Base64URL 编码 + */ + public static String encode(String input) { + return Base64.getUrlEncoder().encodeToString(input.getBytes()); + } + + /** + * Base64URL 解码 + */ + public static String decode(String input) { + return new String(Base64.getUrlDecoder().decode(input)); + } + + /** + * HmacSHA256 加密算法 + * + * @param data 要加密的数据 + * @param secret 秘钥 + */ + public static String HMACSHA256(String data, String secret) throws Exception { + Mac sha256_HMAC = Mac.getInstance("HmacSHA256"); + SecretKeySpec secret_key = new SecretKeySpec(secret.getBytes("UTF-8"), "HmacSHA256"); + sha256_HMAC.init(secret_key); + byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8")); + StringBuilder sb = new StringBuilder(); + for (byte item : array) { + sb.append(Integer.toHexString((item & 0xFF) | 0x100), 1, 3); + } + return sb.toString().toUpperCase(); + } + + + /** + * 获取签名 + */ + public static String getSignature(String payload) throws Exception { + return HMACSHA256(encode(DEFAULT_HEADER) + "." + encode(payload), SECRET); + } + + + /** + * 验证jwt,正确返回载体数据,错误返回null + * + * @param jwt + */ + public static String testJwt(String jwt) throws Exception { + String[] jwts = jwt.split("\\."); + + /* 验证签名 */ + if (!HMACSHA256(jwts[0] + "." + jwts[1], SECRET).equals(jwts[2])) { + return null; + } + /* 验证头部信息 */ + if (!jwts[0].equals(encode(DEFAULT_HEADER))) { + return null; + } + return decode(jwts[1]); + } +} \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html deleted file mode 100644 index 4bad8de..0000000 --- a/src/main/resources/templates/login.html +++ /dev/null @@ -1,30 +0,0 @@ - - - - 请登录 - - -
-
-

- 用户名: - -

-

- 密码: - -

- -

- 记住我: - -

- - - - - -
-
- - \ No newline at end of file