&response_type=code | &response_type=code | ||||
&scope=openid+profile | &scope=openid+profile | ||||
&redirect_uri=http://192.168.11.11:8086/home | &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 | &code_challenge=GoX2z51GyLtItvCxPY4fI0q4pzvOVhHy00xcFGQ20os&code_challenge_method=S256&response_mode=query | ||||
扩展PKCE协议:(&code_challenge=IHicvKyz0IM1do9-3n9QpHf9xVluBshdD1vCD77gV7s&code_challenge_method=S256&response_mode=fragment) | 扩展PKCE协议:(&code_challenge=IHicvKyz0IM1do9-3n9QpHf9xVluBshdD1vCD77gV7s&code_challenge_method=S256&response_mode=fragment) |
package com.tuoheng.config; | package com.tuoheng.config; | ||||
import com.tuoheng.handler.AccessDeniedHandler; | import com.tuoheng.handler.AccessDeniedHandler; | ||||
import com.tuoheng.handler.AuthenticationEntryPoint; | |||||
import com.tuoheng.mapper.UserMapper; | import com.tuoheng.mapper.UserMapper; | ||||
import com.tuoheng.model.dto.UserBaseInfoDto; | import com.tuoheng.model.dto.UserBaseInfoDto; | ||||
import com.tuoheng.service.impl.OidcUserInfoServiceImpl; | import com.tuoheng.service.impl.OidcUserInfoServiceImpl; | ||||
import org.springframework.context.annotation.Bean; | import org.springframework.context.annotation.Bean; | ||||
import org.springframework.context.annotation.Configuration; | import org.springframework.context.annotation.Configuration; | ||||
import org.springframework.core.annotation.Order; | 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.builders.HttpSecurity; | ||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; | 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.ExpressionUrlAuthorizationConfigurer; | ||||
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer; | 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.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.core.userdetails.UserDetailsService; | ||||
import org.springframework.security.oauth2.core.oidc.OidcUserInfo; | import org.springframework.security.oauth2.core.oidc.OidcUserInfo; | ||||
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings; | 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.OidcUserInfoAuthenticationContext; | ||||
import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken; | import org.springframework.security.oauth2.server.authorization.oidc.authentication.OidcUserInfoAuthenticationToken; | ||||
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationToken; | 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.provisioning.JdbcUserDetailsManager; | ||||
import org.springframework.security.web.SecurityFilterChain; | import org.springframework.security.web.SecurityFilterChain; | ||||
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; | import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint; | ||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; | |||||
import org.springframework.security.web.util.matcher.RequestMatcher; | import org.springframework.security.web.util.matcher.RequestMatcher; | ||||
import javax.sql.DataSource; | import javax.sql.DataSource; | ||||
import java.util.Collections; | |||||
import java.util.function.Function; | import java.util.function.Function; | ||||
/** | /** | ||||
.and() | .and() | ||||
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) | .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) | ||||
.exceptionHandling(exceptions -> exceptions | .exceptionHandling(exceptions -> exceptions | ||||
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")) | |||||
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/toLogin")) | |||||
.accessDeniedHandler(new AccessDeniedHandler())) | .accessDeniedHandler(new AccessDeniedHandler())) | ||||
//.authenticationEntryPoint(new AuthenticationEntryPoint())) | //.authenticationEntryPoint(new AuthenticationEntryPoint())) | ||||
.apply(authorizationServerConfigurer) | .apply(authorizationServerConfigurer) | ||||
@Bean | @Bean | ||||
@Order(2) | @Order(2) | ||||
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { | SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { | ||||
//http.addFilterBefore(verifyCodeFilter, UsernamePasswordAuthenticationFilter.class); | |||||
http.addFilterAt(new VerifyCodeFilter(),UsernamePasswordAuthenticationFilter.class); | |||||
http.csrf().disable() | http.csrf().disable() | ||||
.authorizeHttpRequests((authorize) -> authorize | .authorizeHttpRequests((authorize) -> authorize | ||||
.antMatchers("/login", "/getHealth", "/static/**").permitAll() | |||||
.antMatchers("/toLogin", "/getHealth", "/static/**", "/vercode").permitAll() | |||||
.antMatchers("/user/create").permitAll() | .antMatchers("/user/create").permitAll() | ||||
.anyRequest().authenticated() | .anyRequest().authenticated() | ||||
) | ) | ||||
// authorization server filter chain | // authorization server filter chain | ||||
//.formLogin(Customizer.withDefaults()); | //.formLogin(Customizer.withDefaults()); | ||||
.formLogin(form -> | .formLogin(form -> | ||||
form.loginPage("/login") | |||||
form.loginPage("/toLogin") | |||||
.loginProcessingUrl("/login") | .loginProcessingUrl("/login") | ||||
); | ); | ||||
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(); | |||||
} | |||||
} |
package com.tuoheng.constants; | |||||
/** | |||||
* 安全配置常量 | |||||
*/ | |||||
public class CommonConstant { | |||||
public static final String VALIDATE_CODE = "validateCode"; | |||||
} |
package com.tuoheng.controller; | package com.tuoheng.controller; | ||||
import lombok.extern.slf4j.Slf4j; | |||||
import org.springframework.web.bind.annotation.GetMapping; | import org.springframework.web.bind.annotation.GetMapping; | ||||
import org.springframework.web.bind.annotation.RestController; | import org.springframework.web.bind.annotation.RestController; | ||||
* @date 2022/9/28 9:55 | * @date 2022/9/28 9:55 | ||||
*/ | */ | ||||
@RestController | @RestController | ||||
@Slf4j | |||||
public class HealthController { | public class HealthController { | ||||
@GetMapping("/getHealth") | @GetMapping("/getHealth") | ||||
public String getHealth(){ | public String getHealth(){ | ||||
log.info("oidc is ok"); | |||||
return "oidc is ok"; | return "oidc is ok"; | ||||
} | } | ||||
import lombok.extern.slf4j.Slf4j; | import lombok.extern.slf4j.Slf4j; | ||||
import org.springframework.stereotype.Controller; | import org.springframework.stereotype.Controller; | ||||
import org.springframework.ui.Model; | |||||
import org.springframework.web.bind.annotation.GetMapping; | import org.springframework.web.bind.annotation.GetMapping; | ||||
import org.springframework.web.bind.annotation.RequestParam; | |||||
/** | /** | ||||
* @author chenjiandong | * @author chenjiandong | ||||
@Slf4j | @Slf4j | ||||
@Controller | @Controller | ||||
public class Oauth2Controller { | 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"; | return "login"; | ||||
} | } | ||||
} | } |
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()); | |||||
} | |||||
} |
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); | |||||
} | |||||
} |
<contextName>tuoheng_oidc_server</contextName> | <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信息] - [日志信息]--> | <!--日志格式: [时间] [级别] [线程] [行号] [logger信息] - [日志信息]--> | ||||
<property name="logging.pattern" value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%level][%thread][%L] %logger - %msg%n"/> | <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"/> | <property name="logging.charset" value="UTF-8"/> |
color: #FFFFFF; | color: #FFFFFF; | ||||
font-size: 16px; | 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{ | .login__form .form__tips{ | ||||
margin: 0; | margin: 0; | ||||
height: 0; | height: 0; | ||||
<form th:action="@{/login}" method="post"> | <form th:action="@{/login}" method="post"> | ||||
<input name="username" placeholder="请输入用户名" type="text"/> | <input name="username" placeholder="请输入用户名" type="text"/> | ||||
<input name="password" placeholder="请输入密码" type="password"/> | <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 class="form__tips is--error" th:if="${param.error}"> | ||||
用户名密码错误,请重新输入! | 用户名密码错误,请重新输入! | ||||
</div> | </div> | ||||
<!-- <input name="code" placeholder="请输入验证码" value="code" /> --> | |||||
<div class="form__tips is--error" th:if="${param.validerror}"> | |||||
验证码错误,请重新输入! | |||||
</div> | |||||
<button type="submit">登 录</button> | <button type="submit">登 录</button> | ||||
</form> | </form> | ||||
</div> | </div> | ||||
</div> | </div> | ||||
<!--绑定点击事件 --> | |||||
<script> | |||||
const imgDom = document.querySelector('.code__img') | |||||
imgDom.onclick = function() { | |||||
imgDom.src='/vercode' | |||||
} | |||||
</script> | |||||
</body> | </body> | ||||
</html> | </html> |