@@ -5,8 +5,8 @@ client_id=tuoheng-dsp | |||
&response_type=code | |||
&scope=openid+profile | |||
&redirect_uri=http://192.168.11.11:8086/home | |||
&state=4991a0e66547452286dd56e0d9473a0e | |||
&state=http://192.168.11.11:8086/home?url=4991a0e66547452286dd56e0d9473a0e | |||
&code_challenge=GoX2z51GyLtItvCxPY4fI0q4pzvOVhHy00xcFGQ20os&code_challenge_method=S256&response_mode=query | |||
扩展PKCE协议:(&code_challenge=IHicvKyz0IM1do9-3n9QpHf9xVluBshdD1vCD77gV7s&code_challenge_method=S256&response_mode=fragment) |
@@ -1,7 +1,6 @@ | |||
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.impl.OidcUserInfoServiceImpl; | |||
@@ -10,30 +9,24 @@ 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.annotation.authentication.builders.AuthenticationManagerBuilder; | |||
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.config.web.server.ServerHttpSecurity; | |||
import org.springframework.security.core.authority.SimpleGrantedAuthority; | |||
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; | |||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | |||
import org.springframework.security.web.util.matcher.RequestMatcher; | |||
import javax.sql.DataSource; | |||
import java.util.Collections; | |||
import java.util.function.Function; | |||
/** | |||
@@ -81,7 +74,7 @@ public class SecurityConfig { | |||
.and() | |||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) | |||
.exceptionHandling(exceptions -> exceptions | |||
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) | |||
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/toLogin")) | |||
.accessDeniedHandler(new AccessDeniedHandler())) | |||
//.authenticationEntryPoint(new AuthenticationEntryPoint())) | |||
.apply(authorizationServerConfigurer) | |||
@@ -92,9 +85,11 @@ public class SecurityConfig { | |||
@Bean | |||
@Order(2) | |||
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { | |||
//http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class); | |||
http.addFilterAt(new VerifyCodeFilter(),UsernamePasswordAuthenticationFilter.class); | |||
http.csrf().disable() | |||
.authorizeHttpRequests((authorize) -> authorize | |||
.antMatchers("/login", "/getHealth", "/static/**").permitAll() | |||
.antMatchers("/toLogin", "/getHealth", "/static/**", "/vercode").permitAll() | |||
.antMatchers("/user/create").permitAll() | |||
.anyRequest().authenticated() | |||
) | |||
@@ -102,7 +97,7 @@ public class SecurityConfig { | |||
// authorization server filter chain | |||
//.formLogin(Customizer.withDefaults()); | |||
.formLogin(form -> | |||
form.loginPage("/login") | |||
form.loginPage("/toLogin") | |||
.loginProcessingUrl("/login") | |||
); | |||
@@ -0,0 +1,81 @@ | |||
package com.tuoheng.config; | |||
/** | |||
* @author chenjiandong | |||
* @description: TODO | |||
* @date 2022/10/12 9:32 | |||
*/ | |||
import java.io.IOException; | |||
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 javax.servlet.http.HttpSession; | |||
import com.tuoheng.constants.CommonConstant; | |||
import org.springframework.security.authentication.InsufficientAuthenticationException; | |||
import org.springframework.security.core.Authentication; | |||
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; | |||
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; | |||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; | |||
import org.springframework.util.StringUtils; | |||
public class VerifyCodeFilter extends AbstractAuthenticationProcessingFilter { | |||
// 是否开启验证码功能 | |||
private boolean isOpenValidateCode = true; | |||
public VerifyCodeFilter() { | |||
super(new AntPathRequestMatcher("/login", "POST")); | |||
SimpleUrlAuthenticationFailureHandler failedHandler = (SimpleUrlAuthenticationFailureHandler)getFailureHandler(); | |||
failedHandler.setDefaultFailureUrl("/toLogin?validerror"); | |||
} | |||
@Override | |||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { | |||
HttpServletRequest req = (HttpServletRequest) request; | |||
HttpServletResponse res=(HttpServletResponse)response; | |||
if (!requiresAuthentication(req, res)) { | |||
chain.doFilter(request, response); | |||
return; | |||
} | |||
if (isOpenValidateCode) { | |||
if(!checkValidateCode(req, res))return; | |||
} | |||
chain.doFilter(request,response); | |||
} | |||
/** | |||
* 覆盖授权验证方法,这里可以做一些自己需要的session设置操作 | |||
*/ | |||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { | |||
return null; | |||
} | |||
protected boolean checkValidateCode(HttpServletRequest request,HttpServletResponse response) throws IOException, ServletException { | |||
HttpSession session = request.getSession(); | |||
String sessionValidateCode = obtainSessionValidateCode(session); | |||
// 让上一次的验证码失效 | |||
session.setAttribute(CommonConstant.VALIDATE_CODE, null); | |||
String validateCodeParameter = obtainValidateCodeParameter(request); | |||
if (StringUtils.isEmpty(validateCodeParameter) || !sessionValidateCode.equalsIgnoreCase(validateCodeParameter)) { | |||
unsuccessfulAuthentication(request, response, new InsufficientAuthenticationException("输入的验证码不正确")); | |||
return false; | |||
} | |||
return true; | |||
} | |||
private String obtainValidateCodeParameter(HttpServletRequest request) { | |||
Object obj = request.getParameter(CommonConstant.VALIDATE_CODE); | |||
return null == obj ? "" : obj.toString(); | |||
} | |||
protected String obtainSessionValidateCode(HttpSession session) { | |||
Object obj = session.getAttribute(CommonConstant.VALIDATE_CODE); | |||
return null == obj ? "" : obj.toString(); | |||
} | |||
} |
@@ -0,0 +1,11 @@ | |||
package com.tuoheng.constants; | |||
/** | |||
* 安全配置常量 | |||
*/ | |||
public class CommonConstant { | |||
public static final String VALIDATE_CODE = "validateCode"; | |||
} |
@@ -1,5 +1,6 @@ | |||
package com.tuoheng.controller; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.web.bind.annotation.GetMapping; | |||
import org.springframework.web.bind.annotation.RestController; | |||
@@ -9,10 +10,12 @@ import org.springframework.web.bind.annotation.RestController; | |||
* @date 2022/9/28 9:55 | |||
*/ | |||
@RestController | |||
@Slf4j | |||
public class HealthController { | |||
@GetMapping("/getHealth") | |||
public String getHealth(){ | |||
log.info("oidc is ok"); | |||
return "oidc is ok"; | |||
} | |||
@@ -2,7 +2,9 @@ package com.tuoheng.controller; | |||
import lombok.extern.slf4j.Slf4j; | |||
import org.springframework.stereotype.Controller; | |||
import org.springframework.ui.Model; | |||
import org.springframework.web.bind.annotation.GetMapping; | |||
import org.springframework.web.bind.annotation.RequestParam; | |||
/** | |||
* @author chenjiandong | |||
@@ -12,9 +14,22 @@ import org.springframework.web.bind.annotation.GetMapping; | |||
@Slf4j | |||
@Controller | |||
public class Oauth2Controller { | |||
@GetMapping("login") | |||
public String login() { | |||
@GetMapping("toLogin") | |||
public String login(@RequestParam(value = "error", required = false) String error, | |||
@RequestParam(value = "validerror", required = false) String validerror, | |||
@RequestParam(value = "logout", required = false) String logout, Model model) { | |||
if (error != null) { | |||
model.addAttribute("msg", "用户名或密码错误!"); | |||
} | |||
if(validerror!=null){ | |||
model.addAttribute("msg", "验证码错误!"); | |||
} | |||
if (logout != null) { | |||
model.addAttribute("msg", "成功退出!"); | |||
} | |||
return "login"; | |||
} | |||
} |
@@ -0,0 +1,30 @@ | |||
package com.tuoheng.controller; | |||
import com.tuoheng.until.VerifyCode; | |||
import org.springframework.web.bind.annotation.GetMapping; | |||
import org.springframework.web.bind.annotation.RestController; | |||
import javax.servlet.http.HttpServletRequest; | |||
import javax.servlet.http.HttpServletResponse; | |||
import javax.servlet.http.HttpSession; | |||
import java.awt.image.BufferedImage; | |||
import java.io.IOException; | |||
/** | |||
* @author chenjiandong | |||
* @description: TODO | |||
* @date 2022/10/11 17:08 | |||
*/ | |||
@RestController | |||
public class VerifyCodeController { | |||
@GetMapping("/vercode") | |||
public void code(HttpServletRequest req, HttpServletResponse resp) throws IOException { | |||
VerifyCode vc = new VerifyCode(); | |||
BufferedImage image = vc.getImage(); | |||
String text = vc.getText(); | |||
HttpSession session = req.getSession(); | |||
session.setAttribute("validateCode", text); | |||
VerifyCode.output(image, resp.getOutputStream()); | |||
} | |||
} |
@@ -0,0 +1,114 @@ | |||
package com.tuoheng.until; | |||
import javax.imageio.ImageIO; | |||
import java.awt.*; | |||
import java.awt.image.BufferedImage; | |||
import java.io.IOException; | |||
import java.io.OutputStream; | |||
import java.util.Random; | |||
/** | |||
* @author chenjiandong | |||
* @description: TODO | |||
* @date 2022/10/11 17:07 | |||
*/ | |||
public class VerifyCode { | |||
private int width = 100;// 生成验证码图片的宽度 | |||
private int height = 50;// 生成验证码图片的高度 | |||
private String[] fontNames = { "宋体", "楷体", "隶书", "微软雅黑" }; | |||
private Color bgColor = new Color(255, 255, 255);// 定义验证码图片的背景颜色为白色 | |||
private Random random = new Random(); | |||
private String codes = "123456789ABCDEFGHIJKLMNPQRSTUVWXYZ"; | |||
private String text;// 记录随机字符串 | |||
/** | |||
* 获取一个随意颜色 | |||
* | |||
* @return | |||
*/ | |||
private Color randomColor() { | |||
int red = random.nextInt(150); | |||
int green = random.nextInt(150); | |||
int blue = random.nextInt(150); | |||
return new Color(red, green, blue); | |||
} | |||
/** | |||
* 获取一个随机字体 | |||
* | |||
* @return | |||
*/ | |||
private Font randomFont() { | |||
String name = fontNames[random.nextInt(fontNames.length)]; | |||
int style = random.nextInt(4); | |||
int size = random.nextInt(5) + 24; | |||
return new Font(name, style, size); | |||
} | |||
/** | |||
* 获取一个随机字符 | |||
* | |||
* @return | |||
*/ | |||
private char randomChar() { | |||
return codes.charAt(random.nextInt(codes.length())); | |||
} | |||
/** | |||
* 创建一个空白的BufferedImage对象 | |||
* | |||
* @return | |||
*/ | |||
private BufferedImage createImage() { | |||
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); | |||
Graphics2D g2 = (Graphics2D) image.getGraphics(); | |||
g2.setColor(bgColor);// 设置验证码图片的背景颜色 | |||
g2.fillRect(0, 0, width, height); | |||
return image; | |||
} | |||
public BufferedImage getImage() { | |||
BufferedImage image = createImage(); | |||
Graphics2D g2 = (Graphics2D) image.getGraphics(); | |||
StringBuffer sb = new StringBuffer(); | |||
for (int i = 0; i < 4; i++) { | |||
String s = randomChar() + ""; | |||
sb.append(s); | |||
g2.setColor(randomColor()); | |||
g2.setFont(randomFont()); | |||
float x = i * width * 1.0f / 4; | |||
g2.drawString(s, x, height - 15); | |||
} | |||
this.text = sb.toString(); | |||
drawLine(image); | |||
return image; | |||
} | |||
/** | |||
* 绘制干扰线 | |||
* | |||
* @param image | |||
*/ | |||
private void drawLine(BufferedImage image) { | |||
Graphics2D g2 = (Graphics2D) image.getGraphics(); | |||
int num = 5; | |||
for (int i = 0; i < num; i++) { | |||
int x1 = random.nextInt(width); | |||
int y1 = random.nextInt(height); | |||
int x2 = random.nextInt(width); | |||
int y2 = random.nextInt(height); | |||
g2.setColor(randomColor()); | |||
g2.setStroke(new BasicStroke(1.5f)); | |||
g2.drawLine(x1, y1, x2, y2); | |||
} | |||
} | |||
public String getText() { | |||
return text; | |||
} | |||
public static void output(BufferedImage image, OutputStream out) throws IOException { | |||
ImageIO.write(image, "JPEG", out); | |||
} | |||
} |
@@ -13,8 +13,8 @@ | |||
<contextName>tuoheng_oidc_server</contextName> | |||
<!--定义日志变量--> | |||
<!--<property name="logging.path" value="D:\\idealogs\\tuoheng_oidc"/>--> | |||
<property name="logging.path" value="/data/java/logs/tuoheng_oidc"/> | |||
<property name="logging.path" value="D:\\idealogs\\tuoheng_oidc"/> | |||
<!--<property name="logging.path" value="/data/java/logs/tuoheng_oidc"/>--> | |||
<!--日志格式: [时间] [级别] [线程] [行号] [logger信息] - [日志信息]--> | |||
<property name="logging.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%level][%thread][%L] %logger - %msg%n"/> | |||
<property name="logging.charset" value="UTF-8"/> |
@@ -76,6 +76,21 @@ | |||
color: #FFFFFF; | |||
font-size: 16px; | |||
} | |||
.form__code{ | |||
display: flex; | |||
align-items: center; | |||
justify-content: space-between; | |||
margin-bottom: 18px; | |||
} | |||
.form__code input{ | |||
width: calc(100% - 152px); | |||
margin-bottom: 0; | |||
} | |||
.form__code img{ | |||
width: 100px; | |||
height: 42px; | |||
cursor: pointer; | |||
} | |||
.login__form .form__tips{ | |||
margin: 0; | |||
height: 0; | |||
@@ -95,13 +110,26 @@ | |||
<form th:action="@{/login}" method="post"> | |||
<input name="username" placeholder="请输入用户名" type="text"/> | |||
<input name="password" placeholder="请输入密码" type="password"/> | |||
<div class="form__code"> | |||
<input name="validateCode" placeholder="请输入验证码"/> | |||
<img class="code__img" src="/vercode" /> | |||
</div> | |||
<div class="form__tips is--error" th:if="${param.error}"> | |||
用户名密码错误,请重新输入! | |||
</div> | |||
<!-- <input name="code" placeholder="请输入验证码" value="code" /> --> | |||
<div class="form__tips is--error" th:if="${param.validerror}"> | |||
验证码错误,请重新输入! | |||
</div> | |||
<button type="submit">登 录</button> | |||
</form> | |||
</div> | |||
</div> | |||
<!--绑定点击事件 --> | |||
<script> | |||
const imgDom = document.querySelector('.code__img') | |||
imgDom.onclick = function() { | |||
imgDom.src='/vercode' | |||
} | |||
</script> | |||
</body> | |||
</html> |