@@ -88,6 +88,18 @@ | |||
<artifactId>spring-boot-starter-thymeleaf</artifactId> | |||
</dependency> | |||
<dependency> | |||
<groupId>io.jsonwebtoken</groupId> | |||
<artifactId>jjwt</artifactId> | |||
<version>0.9.0</version> | |||
</dependency> | |||
<!-- JSON 解析器和生成器 --> | |||
<dependency> | |||
<groupId>com.alibaba</groupId> | |||
<artifactId>fastjson</artifactId> | |||
<version>1.2.76</version> | |||
</dependency> | |||
</dependencies> | |||
@@ -133,6 +145,9 @@ | |||
<resource> | |||
<directory>src/main/resources</directory> | |||
<filtering>true</filtering> | |||
<includes> | |||
<include>**/*.*</include> | |||
</includes> | |||
</resource> | |||
<resource> |
@@ -1,29 +1,26 @@ | |||
package com.tuoheng.config; | |||
import com.tuoheng.handler.AccessDeniedHandler; | |||
import com.tuoheng.handler.AuthenticationEntryPoint; | |||
import com.tuoheng.mapper.UserMapper; | |||
import com.tuoheng.model.dto.UserBaseInfoDto; | |||
import com.tuoheng.service.OidcUserInfoService; | |||
import com.tuoheng.service.impl.OidcUserInfoServiceImpl; | |||
import lombok.RequiredArgsConstructor; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.context.annotation.Bean; | |||
import org.springframework.context.annotation.Configuration; | |||
import org.springframework.core.annotation.Order; | |||
import org.springframework.security.config.Customizer; | |||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; | |||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | |||
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer; | |||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer; | |||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer; | |||
import org.springframework.security.core.userdetails.User; | |||
import org.springframework.security.core.userdetails.UserDetails; | |||
import org.springframework.security.core.userdetails.UserDetailsService; | |||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo; | |||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; | |||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationContext; | |||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken; | |||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; | |||
import org.springframework.security.provisioning.InMemoryUserDetailsManager; | |||
import org.springframework.security.provisioning.JdbcUserDetailsManager; | |||
import org.springframework.security.web.SecurityFilterChain; | |||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; | |||
@@ -76,8 +73,10 @@ public class SecurityConfig { | |||
}).apply(authorizationServerConfigurer) | |||
.and() | |||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) | |||
.exceptionHandling(exceptions -> exceptions. | |||
authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))) | |||
.exceptionHandling(exceptions -> exceptions | |||
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) | |||
.accessDeniedHandler(new AccessDeniedHandler())) | |||
//.authenticationEntryPoint(new AuthenticationEntryPoint())) | |||
.apply(authorizationServerConfigurer) | |||
.and() | |||
.build(); | |||
@@ -90,6 +89,7 @@ public class SecurityConfig { | |||
.authorizeHttpRequests((authorize) -> authorize | |||
.antMatchers("/getHealth").permitAll() | |||
.antMatchers("/login").permitAll() | |||
.antMatchers("/static/**").permitAll() | |||
.anyRequest().authenticated() | |||
) | |||
// Form login handles the redirect to the login page from the | |||
@@ -104,14 +104,13 @@ public class SecurityConfig { | |||
@Bean | |||
public UserDetailsService userDetailsService() { | |||
UserDetails userDetails = User.withDefaultPasswordEncoder() | |||
/*UserDetails userDetails = User.withDefaultPasswordEncoder() | |||
.username("admin") | |||
.password("123456") | |||
.roles("ADMIN") | |||
.build(); | |||
return new InMemoryUserDetailsManager(userDetails); | |||
//return new JdbcUserDetailsManager(dataSource); | |||
return new InMemoryUserDetailsManager(userDetails);*/ | |||
return new JdbcUserDetailsManager(dataSource); | |||
} | |||
@Bean | |||
@@ -119,9 +118,9 @@ public class SecurityConfig { | |||
return ProviderSettings.builder().build(); | |||
} | |||
// @Bean | |||
// public PasswordEncoder passwordEncoder() { | |||
// return new BCryptPasswordEncoder(); | |||
// } | |||
/*@Bean | |||
public PasswordEncoder passwordEncoder() { | |||
return new BCryptPasswordEncoder(); | |||
}*/ | |||
} |
@@ -0,0 +1,38 @@ | |||
package com.tuoheng.handler; | |||
import com.fasterxml.jackson.databind.ObjectMapper; | |||
import org.springframework.security.access.AccessDeniedException; | |||
import org.springframework.stereotype.Component; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
import java.io.IOException; | |||
import java.util.Date; | |||
import java.util.HashMap; | |||
import java.util.Map; | |||
/** | |||
* @author chenjiandong | |||
* @description: TODO | |||
* @date 2022/10/10 11:55 | |||
*/ | |||
@Component | |||
public class AccessDeniedHandler implements org.springframework.security.web.access.AccessDeniedHandler { | |||
@Override | |||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException { | |||
response.setContentType("application/json;charset=UTF-8"); | |||
Map<String, Object> map = new HashMap<String, Object>(); | |||
map.put("code", 401); | |||
map.put("msg", "权限不足"); | |||
map.put("data", accessDeniedException.getMessage()); | |||
map.put("success", false); | |||
map.put("path", request.getServletPath()); | |||
map.put("timestamp", String.valueOf(new Date().getTime())); | |||
ObjectMapper mapper = new ObjectMapper(); | |||
response.setContentType("application/json"); | |||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | |||
response.getWriter().write(mapper.writeValueAsString(map)); | |||
} | |||
} |
@@ -0,0 +1,86 @@ | |||
package com.tuoheng.handler; | |||
import com.alibaba.fastjson.serializer.SerializerFeature; | |||
import com.fasterxml.jackson.databind.ObjectMapper; | |||
import com.alibaba.fastjson.JSONObject; | |||
import com.tuoheng.until.JsonResult; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.http.MediaType; | |||
import org.springframework.security.authentication.AccountExpiredException; | |||
import org.springframework.security.authentication.BadCredentialsException; | |||
import org.springframework.security.authentication.InsufficientAuthenticationException; | |||
import org.springframework.security.authentication.LockedException; | |||
import org.springframework.security.core.AuthenticationException; | |||
import org.springframework.security.oauth2.jwt.BadJwtException; | |||
import org.springframework.security.oauth2.jwt.JwtValidationException; | |||
import javax.servlet.ServletException; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
import java.io.IOException; | |||
import java.io.PrintWriter; | |||
/** | |||
* @author chenjiandong | |||
* @description: TODO | |||
* @date 2022/10/10 12:00 | |||
*/ | |||
@Slf4j | |||
public class AuthenticationEntryPoint implements org.springframework.security.web.AuthenticationEntryPoint { | |||
@Override | |||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { | |||
if (response.isCommitted()){ | |||
return; | |||
} | |||
Throwable throwable = authException.fillInStackTrace(); | |||
String errorMessage = "认证失败"; | |||
if (throwable instanceof BadCredentialsException){ | |||
errorMessage = "错误的客户端信息"; | |||
}else { | |||
Throwable cause = authException.getCause(); | |||
if (cause instanceof JwtValidationException) { | |||
log.warn("JWT Token 过期,具体内容:" + cause.getMessage()); | |||
errorMessage = "无效的token信息"; | |||
} else if (cause instanceof BadJwtException){ | |||
log.warn("JWT 签名异常,具体内容:" + cause.getMessage()); | |||
errorMessage = "无效的token信息"; | |||
} else if (cause instanceof AccountExpiredException){ | |||
errorMessage = "账户已过期"; | |||
} else if (cause instanceof LockedException){ | |||
errorMessage = "账户已被锁定"; | |||
// } else if (cause instanceof InvalidClientException || cause instanceof BadClientCredentialsException){ | |||
// response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed(401,"无效的客户端"))); | |||
// } else if (cause instanceof InvalidGrantException || cause instanceof RedirectMismatchException){ | |||
// response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("无效的类型"))); | |||
// } else if (cause instanceof UnauthorizedClientException) { | |||
// response.getWriter().write(JSON.toJSONString(SingleResultBundle.failed("未经授权的客户端"))); | |||
} else if (throwable instanceof InsufficientAuthenticationException) { | |||
String message = throwable.getMessage(); | |||
if (message.contains("Invalid token does not contain resource id")){ | |||
errorMessage = "未经授权的资源服务器"; | |||
}else if (message.contains("Full authentication is required to access this resource")){ | |||
errorMessage = "缺少验证信息"; | |||
} | |||
}else { | |||
errorMessage = "验证异常"; | |||
} | |||
} | |||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); | |||
response.setCharacterEncoding("utf-8"); | |||
response.setContentType(MediaType.APPLICATION_JSON_VALUE); | |||
ObjectMapper objectMapper = new ObjectMapper(); | |||
JsonResult jsonResult = new JsonResult(); | |||
String resBody = objectMapper.writeValueAsString(JSONObject.toJSONString(jsonResult.error(401, errorMessage), SerializerFeature.WriteMapNullValue)); | |||
PrintWriter printWriter = response.getWriter(); | |||
printWriter.print(resBody); | |||
printWriter.flush(); | |||
printWriter.close(); | |||
} | |||
} |
@@ -0,0 +1,78 @@ | |||
package com.tuoheng.model.dto; | |||
import org.springframework.security.core.GrantedAuthority; | |||
import org.springframework.security.core.authority.SimpleGrantedAuthority; | |||
import org.springframework.security.core.userdetails.UserDetails; | |||
import java.util.Collection; | |||
import java.util.Collections; | |||
/** | |||
* @author chenjiandong | |||
* @description: TODO | |||
* @date 2022/10/10 10:20 | |||
*/ | |||
public class JwtUser implements UserDetails { | |||
private Integer id; | |||
private String username; | |||
private String password; | |||
private Collection<? extends GrantedAuthority> authorities; | |||
public JwtUser() { | |||
} | |||
// 写一个能直接使用user创建jwtUser的构造器 | |||
public JwtUser(UserBaseInfoDto user) { | |||
id = user.getUserId(); | |||
username = user.getUserName(); | |||
password = user.getPassword(); | |||
authorities = Collections.singleton(new SimpleGrantedAuthority(user.getAuthority())); | |||
} | |||
@Override | |||
public Collection<? extends GrantedAuthority> getAuthorities() { | |||
return authorities; | |||
} | |||
@Override | |||
public String getPassword() { | |||
return password; | |||
} | |||
@Override | |||
public String getUsername() { | |||
return username; | |||
} | |||
@Override | |||
public boolean isAccountNonExpired() { | |||
return true; | |||
} | |||
@Override | |||
public boolean isAccountNonLocked() { | |||
return true; | |||
} | |||
@Override | |||
public boolean isCredentialsNonExpired() { | |||
return true; | |||
} | |||
@Override | |||
public boolean isEnabled() { | |||
return true; | |||
} | |||
@Override | |||
public String toString() { | |||
return "JwtUser{" + | |||
"id=" + id + | |||
", username='" + username + '\'' + | |||
", password='" + password + '\'' + | |||
", authorities=" + authorities + | |||
'}'; | |||
} | |||
} |
@@ -14,6 +14,8 @@ public class UserBaseInfoDto { | |||
private String userName; | |||
private String password; | |||
private String authority; | |||
} |
@@ -0,0 +1,29 @@ | |||
package com.tuoheng.service.impl; | |||
import com.tuoheng.mapper.UserMapper; | |||
import com.tuoheng.model.dto.JwtUser; | |||
import com.tuoheng.model.dto.UserBaseInfoDto; | |||
import org.springframework.beans.factory.annotation.Autowired; | |||
import org.springframework.security.core.userdetails.UserDetails; | |||
import org.springframework.security.core.userdetails.UserDetailsService; | |||
import org.springframework.security.core.userdetails.UsernameNotFoundException; | |||
import org.springframework.stereotype.Service; | |||
/** | |||
* @author chenjiandong | |||
* @description: TODO | |||
* @date 2022/10/10 10:13 | |||
*/ | |||
//@Service | |||
//public class UserDetailsServiceImpl implements UserDetailsService { | |||
// @Autowired | |||
// UserMapper userMapper; | |||
// | |||
// @Override | |||
// public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { | |||
// // 根据用户名去数据库中查询 | |||
// UserBaseInfoDto userBaseInfoDto = userMapper.getUserBaseInfo(username); | |||
// return new JwtUser(userBaseInfoDto); | |||
// } | |||
// | |||
//} |
@@ -4,6 +4,9 @@ server: | |||
spring: | |||
profiles: | |||
active: @package.environment@ | |||
web: | |||
resources: | |||
static-locations: classpath:/ | |||
mybatis: | |||
mapper-locations: classpath*:mapper/*Mapper.xml |
@@ -9,7 +9,7 @@ | |||
<select id="getUserBaseInfo" resultType="com.tuoheng.model.dto.UserBaseInfoDto"> | |||
select a.id as userId, a.username as userName, b.authority | |||
select a.id as userId, a.username as userName, a.password , b.authority | |||
from users a | |||
inner join authorities b on a.id = b.user_id | |||
where a.username = #{username} |
@@ -1,27 +1,95 @@ | |||
<!DOCTYPE html> | |||
<html lang="zh" xmlns:th="http://www.thymeleaf.org"> | |||
<html lang="en"> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<title>登录页面</title> | |||
</head> | |||
<head> | |||
<meta charset="UTF-8"> | |||
<meta name="viewport" content="width=device-width, initial-scale=1.0"> | |||
<title>login</title> | |||
<style> | |||
.login__back{ | |||
position: fixed; | |||
top: 0; | |||
right: 0; | |||
bottom: 0; | |||
left: 0; | |||
background: url('../static/back.png'); | |||
background-position: center center; | |||
background-repeat: no-repeat; | |||
background-attachment: fixed; | |||
background-size: cover; | |||
} | |||
.login__form{ | |||
/* width: 460px; | |||
height: 410px; */ | |||
width: 410px; | |||
height: 350px; | |||
background: url('../static/form.png'); | |||
background-position: center center; | |||
background-repeat: no-repeat; | |||
background-size: cover; | |||
padding: 60px 30px; | |||
color: rgba(255, 255, 255, 1); | |||
position: relative; | |||
left: 50%; | |||
top: 50%; | |||
transform: translate(-50%,-50%); | |||
} | |||
.login__form h2{ | |||
font-size: 28px; | |||
line-height: 25px; | |||
text-align: center; | |||
margin-bottom: 10px; | |||
} | |||
.login__form p{ | |||
font-size: 12px; | |||
text-align: center; | |||
margin-bottom: 36px; | |||
} | |||
.login__form form{ | |||
padding: 0 40px; | |||
display: flex; | |||
flex-direction: column; | |||
background: transparent; | |||
} | |||
form input{ | |||
height: 40px; | |||
margin-bottom: 18px; | |||
border-radius: 6px; | |||
color: #FFFFFF; | |||
padding: 0 16px; | |||
border: 1px solid rgba(255, 255, 255, 0.5); | |||
background: transparent; | |||
} | |||
form input:focus-within{ | |||
outline: 0; | |||
border: 1px solid #08EBFE; | |||
} | |||
button{ | |||
height: 46px; | |||
border: none; | |||
border-radius: 6px; | |||
background: linear-gradient(0deg, #08EBFE 0%, #28BAC1 100%); | |||
margin-top: 20px; | |||
color: #FFFFFF; | |||
font-size: 16px; | |||
} | |||
</style> | |||
</head> | |||
<body> | |||
<h3>登录</h3> | |||
<form th:action="@{/login}" method="post"> | |||
<table> | |||
<tr> | |||
<td>用户名:</td> | |||
<td><input type="text" name="username"></td> | |||
</tr> | |||
<tr> | |||
<td>密码:</td> | |||
<td><input type="password" name="password"></td> | |||
</tr> | |||
<tr> | |||
<td colspan="2"> | |||
<button type="submit">登录</button> | |||
</td> | |||
</tr> | |||
</table> | |||
</form> | |||
<div class="login__back"> | |||
<div class="login__form"> | |||
<h2>拓恒统一登录平台</h2> | |||
<p>TUOHENG LOGIN PLATFORM</p> | |||
<form th:action="@{/login}" method="post"> | |||
<input name="username" placeholder="请输入用户名" type="text"/> | |||
<input name="password" placeholder="请输入密码" type="password"/> | |||
<!-- <input name="code" placeholder="请输入验证码" value="code" /> --> | |||
<button type="submit">登 录</button> | |||
</form> | |||
</div> | |||
</div> | |||
</body> | |||
</html> |