认证--JSON
课程计划
-
登录成功/失败之后返回json字符串
-
未登录错误提示
-
退出登录json提示
-
获取个人信息/修改个人信息
-
JSON登录
-
手机号验证码登录
一、登录成功/失败返回JSON
1、修改第一个版本的代码
- 直接编写返回的json字符串
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//http对象,支持链式调用
//关闭csrf 跨域请求伪造的控制
http.csrf(csrf -> csrf.disable());
http.authorizeHttpRequests(
auth ->
auth.requestMatchers("/loginpage.html", "/login/**")
.permitAll()
.anyRequest().authenticated() //其他页面,要登录之后才能访问
);//放过登录接口,以及静态页面
// ↓配置表单提交
http.formLogin(form -> {
form.loginPage("/loginpage.html") //自定义登录页面的路径
.loginProcessingUrl("/javasmlogin") //表单提交的路径
.usernameParameter("uname") //自定义用户名的参数名(默认是username)
.passwordParameter("pwd")
//Authentication 是 UsernamePasswordAuthenticationToken-- principal实际的值,UserDetails
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
R ok = R.ok(authentication);
//写出去
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(ok));
writer.flush();
writer.close();
}
})
//AuthenticationException 包含了 登录失败之后的 异常信息
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
R error = R.error(exception.getMessage());
//写出去
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(error));
writer.flush();
writer.close();
}
})
.permitAll(); //以上提到的路径,都放行
});
//注销登录
http.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/loginpage.html")//注销成功之后,跳转的路径
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
//指定加密算法
return new BCryptPasswordEncoder();
}
}
2、优化代码
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//http对象,支持链式调用
//关闭csrf 跨域请求伪造的控制
http.csrf(csrf -> csrf.disable());
http.authorizeHttpRequests(
auth ->
auth.requestMatchers("/loginpage.html", "/login/**")
.permitAll()
.anyRequest().authenticated() //其他页面,要登录之后才能访问
);//放过登录接口,以及静态页面
// ↓配置表单提交
http.formLogin(form -> {
form.loginPage("/loginpage.html") //自定义登录页面的路径
.loginProcessingUrl("/javasmlogin") //表单提交的路径
.usernameParameter("uname") //自定义用户名的参数名(默认是username)
.passwordParameter("pwd")
//Authentication 是 UsernamePasswordAuthenticationToken-- principal实际的值,UserDetails
.successHandler((request, response, authentication) ->
createSuccessJson(response,authentication)
)
//AuthenticationException 包含了 登录失败之后的 异常信息
.failureHandler((request, response, exception) ->
createFailJson(response,exception)
)
.permitAll(); //以上提到的路径,都放行
});
//注销登录
http.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/loginpage.html")//注销成功之后,跳转的路径
.permitAll()
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
//指定加密算法
return new BCryptPasswordEncoder();
}
private void createSuccessJson(HttpServletResponse response,Object object){
R ok = R.ok(object);
createJson(response,ok);
}
private void createFailJson(HttpServletResponse response,AuthenticationException e){
R error = R.error(e.getMessage());
createJson(response,error);
}
private void createJson(HttpServletResponse response,R r){
try {
response.setContentType("application/json;charset=utf-8");
//写出去
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(r));
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
二、配置未登录
异常信息的处理
未配置的情况下,会拦截请求,跳转到登录页面
http.exceptionHandling().authenticationEntryPoint((request,response,e)->
createFailJson(response,"当前用户未登录,请先登录再访问"));
三、退出登录
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
//http对象,支持链式调用
//关闭csrf 跨域请求伪造的控制
http.csrf(csrf -> csrf.disable());
http.authorizeHttpRequests(
auth ->
auth.requestMatchers("/loginpage.html", "/login/**")
.permitAll()
.anyRequest().authenticated() //其他页面,要登录之后才能访问
);//放过登录接口,以及静态页面
// ↓配置表单提交
http.formLogin(form -> {
form.loginPage("/loginpage.html") //自定义登录页面的路径
.loginProcessingUrl("/javasmlogin") //表单提交的路径
.usernameParameter("uname") //自定义用户名的参数名(默认是username)
.passwordParameter("pwd")
//Authentication 是 UsernamePasswordAuthenticationToken-- principal实际的值,UserDetails
.successHandler((request, response, authentication) ->
createSuccessJson(response,authentication)
)
//AuthenticationException 包含了 登录失败之后的 异常信息
.failureHandler((request, response, exception) ->
createFailJson(response,exception)
)
.permitAll(); //以上提到的路径,都放行
});
//注销登录
http.logout(logout -> logout
.logoutUrl("/logout")
//退出登录的时候,返回用户信息
.logoutSuccessHandler((r,response,a)->createSuccessJson(response,"退出登录成功!"))
.permitAll()
);
//未登录异常提示
http.exceptionHandling().authenticationEntryPoint((request,response,e)->
createFailJson(response,"当前用户未登录,请先登录再访问"));
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
//指定加密算法
return new BCryptPasswordEncoder();
}
private void createSuccessJson(HttpServletResponse response,Object object){
R ok = R.ok(object);
createJson(response,ok);
}
private void createFailJson(HttpServletResponse response,AuthenticationException e){
R error = R.error(e.getMessage());
createJson(response,error);
}
private void createFailJson(HttpServletResponse response,String e){
R error = R.error(e);
createJson(response,error);
}
private void createJson(HttpServletResponse response,R r){
try {
response.setContentType("application/json;charset=utf-8");
//如果想修改返回的状态码,可以这么修改
//response.setStatus(r.getCode());
//写出去
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(r));
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
四、获取当前登录的用户信息
已经登录之后,获取当前登录的用户信息
@GetMapping("/my")
public R queryMyUser(){
LoginUserDetails loginUserDetails = loginService.getLoginUser();
return R.ok(loginUserDetails);
}
@Override
public LoginUserDetails getLoginUser() {
//查询已经登录的用户信息
//获取Security上下文对象
SecurityContext context = SecurityContextHolder.getContext();
//从上下文对象中,获取用户信息。UsernamePasswordAuthenticationToken
Authentication authentication = context.getAuthentication();
if (authentication == null){
throw new JavasmException(ExceptionEnum.Not_Login);
}
//principal 未登录成功之前,在UsernamePasswordAuthenticationFilter中,赋值是用户名
//登录成功之后,在DaoAuthenticationProvider中,赋值是UserDetails
Object principal = authentication.getPrincipal();
if (principal instanceof LoginUserDetails){
LoginUserDetails loginUserDetails = (LoginUserDetails) principal;
return loginUserDetails;
}else {
throw new JavasmException(ExceptionEnum.Not_Login);
}
}
五、修改当前登录的用户信息
@PutMapping("/update/loginuser")
public R updateLoginUser(AdminUser adminUser){
loginService.updateLoginUser(adminUser);
return R.ok();
}
@Override
public void updateLoginUser(AdminUser adminUser) {
//修改数据
//先获取个人信息,当前登录的用户数据
LoginUserDetails loginUser = getLoginUser();
if (loginUser != null){
AdminUser loginAdminUser = loginUser.getAdminUser();
Integer uid = loginAdminUser.getUid();
adminUser.setUid(uid);
//密码
if (!StringUtils.isEmpty(adminUser.getPassword())){
//如果传了 密码,密码需要加密之后再保存到数据库
String encodePassword = passwordEncoder.encode(adminUser.getPassword());
adminUser.setPassword(encodePassword);
}
//存储成功
adminUser.updateById();
//上面只是修改了数据库的信息,同步Security中的用户信息
//更新的对象 adminuser中不一定包括所有的字段,从数据库中,查询出最新的数据,同步到Security中
AdminUser newAdminUser = adminUserService.getById(uid);
LoginUserDetails loginUserDetails = new LoginUserDetails(newAdminUser);
UsernamePasswordAuthenticationToken authentication =
UsernamePasswordAuthenticationToken.authenticated(
loginUserDetails, //最新的登录用户信息
newAdminUser.getPassword(), // 最新的密码
loginUser.getAuthorities() //授权列表
);
//获取上下文对象--并把新的Authentication对象存储起来
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
六、JSON格式登录
1、普通版本
确认一下登录接口的名字和数据格式
/jsonlogin
{
"username":"test11",
"password":"123"
}
这个请求方式,本质上,依然是用户名和密码登录,唯一改变的是获取用户名和密码的方式
1.1 新建一个Filter
public class JsonFilter extends UsernamePasswordAuthenticationFilter {
public static final String SPRING_SECURITY_JSON_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_JSON_PASSWORD_KEY = "password";
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else if (!request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)){
throw new AuthenticationServiceException("请求的类型不对,应该是JSON格式");
} else {
//request对象中,可以获取用户名和密码
ServletInputStream inputStream = request.getInputStream();
//字节流转成字符流
InputStreamReader streamReader = new InputStreamReader(inputStream);
//转成字符流
BufferedReader bufferedReader = new BufferedReader(streamReader);
//存储字符的StringBuffer
StringBuffer jsonBuffer = new StringBuffer();
String line;
while ((line = bufferedReader.readLine()) != null){
jsonBuffer.append(line);
}
//用户传入的json字符串
String json = jsonBuffer.toString().trim();
if (StringUtils.isEmpty(json)){
throw new AuthenticationServiceException("参数为空");
}
//把数据转成map
Map<String,String> map = JSONObject.parseObject(json, Map.class);
String username = map.get(SPRING_SECURITY_JSON_USERNAME_KEY);
username = username != null ? username.trim() : "";
String password = map.get(SPRING_SECURITY_JSON_PASSWORD_KEY);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}
1.2 把JsonFilter 配置到过滤器链中
- 修改配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig {
//****************JSON登录--开始****************//
@Resource(name = "usernameLoginUserDetailsService")
UserDetailsService userDetailsService;
@Bean
public DaoAuthenticationProvider jsonProvider() {
DaoAuthenticationProvider authenticationProvider =
new DaoAuthenticationProvider();
//配置 由哪个UserDetailsService负责查询用户信息UserDetails
authenticationProvider.setUserDetailsService(userDetailsService);
//配置密码的加密方式
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
//JSON等于专属的AuthenticationManager
@Bean
public AuthenticationManager jsonAuthenticationManager() {
return new ProviderManager(List.of(jsonProvider()));
}
//配置JsonFilter
public JsonFilter jsonFilter() {
JsonFilter jsonFilter = new JsonFilter();
//配置 AuthenticationManager
jsonFilter.setAuthenticationManager(jsonAuthenticationManager());
//json登录成功之后的显示
jsonFilter.setAuthenticationSuccessHandler(this::createSuccessJson);
//json登录失败之后,
jsonFilter.setAuthenticationFailureHandler(
(r, response, e) ->
createFailJson(response, e)
);
//配置json登录的路径
jsonFilter.setFilterProcessesUrl("/jsonlogin");
return jsonFilter;
}
@Bean
public SecurityContextRepository securityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
//****************JSON登录--结束****************//
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.securityContext(context -> context.securityContextRepository(securityContextRepository()));
//因为当前json登录功能,和用户名密码登录功能类似,
// 所以把jsonfilter放到UsernamePasswordAuthenticaitonFilter相同的位置
http.addFilterAt(jsonFilter(), UsernamePasswordAuthenticationFilter.class);
//关闭csrf 跨域请求伪造的控制
http.csrf(csrf -> csrf.disable());
http.authorizeHttpRequests(
auth ->
auth.requestMatchers("/loginpage.html", "/login/**")
.permitAll()
.anyRequest().authenticated() //其他页面,要登录之后才能访问
);//放过登录接口,以及静态页面
// ↓配置表单提交
http.formLogin(form -> {
form.loginPage("/loginpage.html") //自定义登录页面的路径
.loginProcessingUrl("/javasmlogin") //表单提交的路径
.usernameParameter("uname") //自定义用户名的参数名(默认是username)
.passwordParameter("pwd")
//Authentication 是 UsernamePasswordAuthenticationToken-- principal实际的值,UserDetails
.successHandler(this::createSuccessJson)
//AuthenticationException 包含了 登录失败之后的 异常信息
.failureHandler((request, response, exception) ->
createFailJson(response, exception)
)
.permitAll(); //以上提到的路径,都放行
});
//注销登录
http.logout(logout -> logout
.logoutUrl("/logout")
//退出登录的时候,返回用户信息
.logoutSuccessHandler((r, response, a) -> createSuccessJson(response, "退出登录成功!"))
.permitAll()
);
//未登录异常提示
http.exceptionHandling().authenticationEntryPoint((request, response, e) ->
createFailJson(response, "当前用户未登录,请先登录再访问"));
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
//指定加密算法
return new BCryptPasswordEncoder();
}
private void createSuccessJson(HttpServletRequest request,HttpServletResponse response, Authentication authentication) {
SecurityContextHolderStrategy strategy = SecurityContextHolder.getContextHolderStrategy();
strategy.getContext().setAuthentication(authentication);
securityContextRepository().saveContext(strategy.getContext(), request, response);
createSuccessJson(response,authentication);
}
private void createSuccessJson(HttpServletResponse response, Object object) {
R ok = R.ok(object);
createJson(response, ok);
}
private void createFailJson(HttpServletResponse response, AuthenticationException e) {
R error = R.error(e.getMessage());
createJson(response, error);
}
private void createFailJson(HttpServletResponse response, String e) {
R error = R.error(e);
createJson(response, error);
}
private void createJson(HttpServletResponse response, R r) {
try {
response.setContentType("application/json;charset=utf-8");
//如果想修改返回的状态码,可以这么修改
//response.setStatus(r.getCode());
//写出去
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(r));
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
2、简单版
2.1 正常的Controller
@PostMapping("/json")
public R doJsonLogin(@RequestBody AdminUser adminUser){
ParameterUtils.checkParameter(adminUser);
ParameterUtils.checkParameter(adminUser.getUsername(),adminUser.getPassword());
//import org.springframework.security.core.Authentication;
Authentication authentication = loginService.doJsonLogin(adminUser);
return R.ok(authentication);
}
2.2 正常的Service
@Resource
HttpSession httpSession;
@Resource
HttpServletRequest request;
@Resource
HttpServletResponse response;
@Resource
SecurityContextRepository securityContextRepository;
@Override
public Authentication doJsonLogin(AdminUser adminUser) {
//获取用户名和密码
String username = adminUser.getUsername();
String password = adminUser.getPassword();
//根据用户名 查询用户信息
AdminUser loginAdminUser = adminUserService.getByUsername(username);
if (loginAdminUser == null) {
throw new JavasmException(ExceptionEnum.User_Not_Found);
}
//判断 密码是否正确
if (!this.passwordEncoder.matches(password, loginAdminUser.getPassword())) {
throw new JavasmException(ExceptionEnum.Password_Error);
}
//登录成功了--以下的代码不能放入多线程中
//Security已经做了线程安全的设置,如果使用子线程存储数据,是无法获取主线程中的对象的
UserDetails userDetails = new LoginUserDetails(loginAdminUser);
//创建一个新的 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authenticated =
UsernamePasswordAuthenticationToken.authenticated(
userDetails,
loginAdminUser.getPassword(),
userDetails.getAuthorities()
);
//登录成功的标志,存储到上下文对象中
//SecurityContextHolder.getContext().setAuthentication(authenticated);
SecurityContextHolderStrategy strategy = SecurityContextHolder.getContextHolderStrategy();
strategy.getContext().setAuthentication(authenticated);
securityContextRepository.saveContext(strategy.getContext(), request, response);
return authenticated;
}
七、图片验证码
在json登录的案例中,添加图片验证码
1、引入第三方依赖
<!--图片验证码-->
<dependency>
<groupId>com.github.whvcse</groupId>
<artifactId>easy-captcha</artifactId>
<version>1.6.2</version>
</dependency>
<!--手动引入Nashorn Javascript引擎-->
<dependency>
<groupId>org.openjdk.nashorn</groupId>
<artifactId>nashorn-core</artifactId>
<version>15.4</version>
</dependency>
2、生成图片接口
@Resource
HttpServletResponse response;
@Resource
HttpSession httpSession;
@GetMapping("/imgcode")
public void createImageCode() throws IOException {
//ArithmeticCaptcha 算术验证码
//SpecCaptcha 英文验证码
// ChineseCaptcha 中文验证码
// GifCaptcha 动态图片
ArithmeticCaptcha captcha = new ArithmeticCaptcha(150,50);
//SpecCaptcha captcha = new SpecCaptcha(150,50);
//ChineseCaptcha captcha = new ChineseCaptcha(150,50);
//GifCaptcha captcha = new GifCaptcha(150,50);
//几个数字的算术题
captcha.setLen(2);
//获取验证码的结果--算术题的答案
String code = captcha.text();
//验证码的内存,存储到Session对象中,等待调用
httpSession.setAttribute("img_code",code);
//输出图片
captcha.out(response.getOutputStream());
}
3、修改原有登录代码
public class JsonFilter extends UsernamePasswordAuthenticationFilter {
public static final String SPRING_SECURITY_JSON_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_JSON_PASSWORD_KEY = "password";
@SneakyThrows
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else if (!request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
throw new AuthenticationServiceException("请求的类型不对,应该是JSON格式");
} else {
//request对象中,可以获取用户名和密码
ServletInputStream inputStream = request.getInputStream();
//字节流转成字符流
InputStreamReader streamReader = new InputStreamReader(inputStream);
//转成字符流
BufferedReader bufferedReader = new BufferedReader(streamReader);
//存储字符的StringBuffer
StringBuffer jsonBuffer = new StringBuffer();
String line;
while ((line = bufferedReader.readLine()) != null) {
jsonBuffer.append(line);
}
//用户传入的json字符串
String json = jsonBuffer.toString().trim();
if (StringUtils.isEmpty(json)) {
throw new AuthenticationServiceException("参数为空");
}
//把数据转成map
Map<String, String> map = JSONObject.parseObject(json, Map.class);
String code = map.get("code");
Object imgCode = request.getSession().getAttribute("img_code");
if (code.isEmpty() || imgCode == null ||
imgCode.toString().isEmpty() || !code.equals(imgCode)) {
throw new AuthenticationServiceException("验证码错误");
}
String username = map.get(SPRING_SECURITY_JSON_USERNAME_KEY);
username = username != null ? username.trim() : "";
String password = map.get(SPRING_SECURITY_JSON_PASSWORD_KEY);
password = password != null ? password : "";
UsernamePasswordAuthenticationToken authRequest =
UsernamePasswordAuthenticationToken.unauthenticated(username, password);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
}
- 简单版
@Resource
HttpSession httpSession;
@Resource
HttpServletRequest request;
@Resource
HttpServletResponse response;
@Resource
SecurityContextRepository securityContextRepository;
@Override
public Authentication doJsonLogin(AdminUser adminUser) {
String code = adminUser.getCode();
Object imgCode = httpSession.getAttribute("img_code");
if (code.isEmpty() || imgCode == null ||
imgCode.toString().isEmpty() || !code.equals(imgCode)) {
throw new JavasmException(ExceptionEnum.Code_Error);
}
//获取用户名和密码
String username = adminUser.getUsername();
String password = adminUser.getPassword();
//根据用户名 查询用户信息
AdminUser loginAdminUser = adminUserService.getByUsername(username);
if (loginAdminUser == null) {
throw new JavasmException(ExceptionEnum.User_Not_Found);
}
//判断 密码是否正确
if (!this.passwordEncoder.matches(password, loginAdminUser.getPassword())) {
throw new JavasmException(ExceptionEnum.Password_Error);
}
//登录成功了--以下的代码不能放入多线程中
//Security已经做了线程安全的设置,如果使用子线程存储数据,是无法获取主线程中的对象的
UserDetails userDetails = new LoginUserDetails(loginAdminUser);
//创建一个新的 UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authenticated =
UsernamePasswordAuthenticationToken.authenticated(
userDetails,
loginAdminUser.getPassword(),
userDetails.getAuthorities()
);
//登录成功的标志,存储到上下文对象中
//SecurityContextHolder.getContext().setAuthentication(authenticated);
SecurityContextHolderStrategy strategy = SecurityContextHolder.getContextHolderStrategy();
strategy.getContext().setAuthentication(authenticated);
securityContextRepository.saveContext(strategy.getContext(), request, response);
return authenticated;
}
@TableField(exist = false)
private String code;
八、手机号验证码
1、发送验证码
@GetMapping("/phone/code")
public R sendPhoneCode(String phone){
String code = loginService.sendPhoneCode(phone);
return R.ok(code);
}
@Resource
RedisTemplate<String,Object> redisTemplate;
@Override
public String sendPhoneCode(String phone) {
//生成一个随机数
String code = RandomUtil.getCode(4);
//TODO:发送到手机
//值存入Redis
String key = String.format(RedisKeys.AdminUserPhone,phone);
redisTemplate.opsForValue().set(key,code,10, TimeUnit.MINUTES);
return code;
}
2、模仿用户名密码登录,写一套手机号验证码登录
-
Filter
-
Authentication
-
Provider
-
UserDetailsService
-
修改配置文件
/phoneLogin
{
"phone":"11111",
"code":"1234"
}
3、PhoneFilter
用来接收参数
public class PhoneFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_PHONE_KEY = "phone";
public static final String SPRING_SECURITY_CODE_KEY = "code";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER =
new AntPathRequestMatcher("/phoneLogin", "POST");
public PhoneFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public PhoneFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}else if (!request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
throw new AuthenticationServiceException("请求的类型不对,应该是JSON格式");
} else {
//获取json参数
Map<String, String> map = RequestJsonUtil.getRequestJson(request);
String phone = map.get(SPRING_SECURITY_PHONE_KEY);
phone = phone != null ? phone.trim() : "";
String code = map.get(SPRING_SECURITY_CODE_KEY);
code = code != null ? code : "";
PhoneAuthenticationToken authRequest =
PhoneAuthenticationToken.unauthenticated(phone,code);
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
}
protected void setDetails(HttpServletRequest request, PhoneAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
}
4、PhoneAuthenticationToken
public class PhoneAuthenticationToken extends AbstractAuthenticationToken {
@Getter
private String phone;
@Getter
private String code;
private UserDetails userDetails;
//未登录成功之前,调用的
public PhoneAuthenticationToken(String phone, String code) {
super((Collection)null);
this.phone = phone;
this.code = code;
this.setAuthenticated(false);
}
//登录成功之后,用来向Filter 回值的
public PhoneAuthenticationToken(UserDetails userDetails) {
super(userDetails.getAuthorities());
this.userDetails = userDetails;
this.code = userDetails.getPassword();
//保证这里
super.setAuthenticated(true);
}
public static PhoneAuthenticationToken unauthenticated(String phone, String code) {
return new PhoneAuthenticationToken(phone, code);
}
public static PhoneAuthenticationToken authenticated(UserDetails userDetails) {
return new PhoneAuthenticationToken(userDetails);
}
@Override
public Object getCredentials() {
return this.code;
}
@Override
public Object getPrincipal() {
return this.userDetails;
}
}
5、PhoneProvider
@Component
public class PhoneProvider implements AuthenticationProvider {
@Resource
RedisTemplate<String,String> redisTemplate;
@Resource(name = "phoneLoginUserDetailsService")
UserDetailsService userDetailsService;
//Authentication → PhoneAuthenticationToken
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
PhoneAuthenticationToken phoneAuthenticationToken =
(PhoneAuthenticationToken) authentication;
//获取手机号
String phone = phoneAuthenticationToken.getPhone();
//获取验证码
String code = phoneAuthenticationToken.getCode();
//校验 验证码是否正确
String key = String.format(RedisKeys.AdminUserPhone,phone);
//从Redis中获取正确的验证码
String realCode = redisTemplate.opsForValue().get(key);
if (realCode == null){
throw new BadCredentialsException("验证码过期");
}
//判断用户传入的验证码 是否正确
if (!realCode.equals(code)){
throw new BadCredentialsException("验证码错误");
}
//删除验证码
redisTemplate.delete(key);
//到这里 说明验证码正确---根据手机号,查询UserDetails
UserDetails userDetails = userDetailsService.loadUserByUsername(phone);
//初始化 Authentication 设置为 已经登录
PhoneAuthenticationToken authRequest =
PhoneAuthenticationToken.authenticated(userDetails);
//Details
authRequest.setDetails(phoneAuthenticationToken.getDetails());
return authRequest;
}
@Override
public boolean supports(Class<?> authentication) {
//判断 传入的Class类,是不是当前Provider要处理的Authentication
return PhoneAuthenticationToken.class.isAssignableFrom(authentication);
}
}
6、UserDetailsService
@Service("phoneLoginUserDetailsService")
public class PhoneLoginUserDetailsServiceImpl implements UserDetailsService {
@Resource
AdminUserService adminUserService;
@Override
public UserDetails loadUserByUsername(String phone) throws UsernameNotFoundException {
AdminUser adminUser = adminUserService.getByPhone(phone);
if (adminUser != null){
return new LoginUserDetails(adminUser);
}
return null;
}
}
7、修改配置文件
import com.alibaba.fastjson2.JSON;
import com.javasm.securitydemo.common.exception.R;
import com.javasm.securitydemo.login.json.JsonFilter;
import com.javasm.securitydemo.login.json.PhoneProvider;
import com.javasm.securitydemo.login.phone.PhoneFilter;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
//****************JSON登录--开始****************//
@Resource(name = "usernameLoginUserDetailsService")
UserDetailsService userDetailsService;
@Bean
public DaoAuthenticationProvider jsonProvider() {
DaoAuthenticationProvider authenticationProvider =
new DaoAuthenticationProvider();
//配置 由哪个UserDetailsService负责查询用户信息UserDetails
authenticationProvider.setUserDetailsService(userDetailsService);
//配置密码的加密方式
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
//JSON等于专属的AuthenticationManager
@Bean("jsonAuthManager")
public AuthenticationManager jsonAuthenticationManager() {
return new ProviderManager(List.of(jsonProvider()));
}
//配置JsonFilter
public JsonFilter jsonFilter() {
JsonFilter jsonFilter = new JsonFilter();
//配置 AuthenticationManager
jsonFilter.setAuthenticationManager(jsonAuthenticationManager());
//json登录成功之后的显示
jsonFilter.setAuthenticationSuccessHandler(this::createSuccessJson);
//json登录失败之后,
jsonFilter.setAuthenticationFailureHandler(
(r, response, e) ->
createFailJson(response, e)
);
//配置json登录的路径
jsonFilter.setFilterProcessesUrl("/jsonlogin");
return jsonFilter;
}
//****************JSON登录--结束****************//
//****************Phone登录--开始****************//
@Resource
PhoneProvider phoneProvider;
//手机专属的 AuthenticationManger
@Bean("phoneAuthManager")
public AuthenticationManager phoneAuthManager() {
return new ProviderManager(List.of(phoneProvider));
}
@Autowired
public void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(phoneProvider);
}
@Bean
public PhoneFilter phoneFilter() {
PhoneFilter phoneFilter = new PhoneFilter();
//配置ProviderManager
phoneFilter.setAuthenticationManager(phoneAuthManager());
//json登录成功之后的显示
phoneFilter.setAuthenticationSuccessHandler(this::createSuccessJson);
//json登录失败之后,
phoneFilter.setAuthenticationFailureHandler(
(r, response, e) ->
createFailJson(response, e)
);
return phoneFilter;
}
//****************Phone登录--结束****************//
@Bean
public SecurityContextRepository securityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.securityContext(context -> context.securityContextRepository(securityContextRepository()));
//因为当前json登录功能,和用户名密码登录功能类似,
// 所以把jsonfilter放到UsernamePasswordAuthenticaitonFilter相同的位置
http.addFilterAt(jsonFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAt(phoneFilter(), UsernamePasswordAuthenticationFilter.class);
//关闭csrf 跨域请求伪造的控制
http.csrf(csrf -> csrf.disable());
http.authorizeHttpRequests(
auth ->
auth.requestMatchers("/loginpage.html", "/login/**", "/jsonlogin", "/phoneLogin")
.permitAll()
.anyRequest().authenticated() //其他页面,要登录之后才能访问
);//放过登录接口,以及静态页面
// ↓配置表单提交
http.formLogin(form -> {
// 确保认证信息被正确设置并保存到session中
form.loginPage("/loginpage.html") //自定义登录页面的路径
.loginProcessingUrl("/javasmlogin") //表单提交的路径
.usernameParameter("uname") //自定义用户名的参数名(默认是username)
.passwordParameter("pwd")
//Authentication 是 UsernamePasswordAuthenticationToken-- principal实际的值,UserDetails
.successHandler(this::createSuccessJson)
//AuthenticationException 包含了 登录失败之后的 异常信息
.failureHandler((request, response, exception) ->
createFailJson(response, exception)
)
.permitAll(); //以上提到的路径,都放行
}).authenticationManager(jsonAuthenticationManager());
//注销登录
http.logout(logout -> logout
.logoutUrl("/logout")
//退出登录的时候,返回用户信息
.logoutSuccessHandler((r, response, a) -> createSuccessJson(response, "退出登录成功!"))
.permitAll()
);
//未登录异常提示
http.exceptionHandling().authenticationEntryPoint((request, response, e) ->
createFailJson(response, "当前用户未登录,请先登录再访问"));
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
//指定加密算法
return new BCryptPasswordEncoder();
}
private void createSuccessJson(HttpServletRequest request,HttpServletResponse response, Authentication authentication) {
SecurityContextHolderStrategy strategy = SecurityContextHolder.getContextHolderStrategy();
strategy.getContext().setAuthentication(authentication);
securityContextRepository().saveContext(strategy.getContext(), request, response);
createSuccessJson(response,authentication);
}
private void createSuccessJson(HttpServletResponse response, Object object) {
R ok = R.ok(object);
createJson(response, ok);
}
private void createFailJson(HttpServletResponse response, AuthenticationException e) {
R error = R.error(e.getMessage());
createJson(response, error);
}
private void createFailJson(HttpServletResponse response, String e) {
R error = R.error(e);
createJson(response, error);
}
private void createJson(HttpServletResponse response, R r) {
try {
response.setContentType("application/json;charset=utf-8");
//如果想修改返回的状态码,可以这么修改
//response.setStatus(r.getCode());
//写出去
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(r));
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
九、跨域
添加一个跨域的过滤器
放到Security过滤器链中
必须在登录的过滤器前面
以前的跨域方式就失效了
- 配置SpringMVC的Cors映射
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")//允许所有的 路径
.allowedOriginPatterns("*")
.allowedMethods("GET","POST","PUT","DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);//预检测请求缓冲时间
}
}
- 配置SpringSecurity的跨域支持
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOriginPattern("*");//允许所有的域名
corsConfiguration.addAllowedMethod("*");//允许所有的请求头
corsConfiguration.setAllowCredentials(true);//允许携带Cookie
corsConfiguration.setMaxAge(3600L);//预检测请求缓冲时间
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//对所有的路径应用配置
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
十、修复bug
表单提交登录,在写了JSON登录之后,就会失效。
找不到
{
"code": 500,
"msg": "No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken",
"timestamp": "2025-12-10T15:46:27.179768"
}
因为缺少了AuthenticationManager,找不到处理UsernamePasswordAuthenticationToken的Provider对象
修改配置,添加Form表单提交需要的AuthenticationManager
http.formLogin(form -> {
form.loginPage("/loginpage.html") //自定义登录页面的路径
.loginProcessingUrl("/javasmlogin") //表单提交的路径
.usernameParameter("uname") //自定义用户名的参数名(默认是username)
.passwordParameter("pwd")
//Authentication 是 UsernamePasswordAuthenticationToken-- principal实际的值,UserDetails
.successHandler((request, response, authentication) ->
createSuccessJson(response, authentication)
)
//AuthenticationException 包含了 登录失败之后的 异常信息
.failureHandler((request, response, exception) ->
createFailJson(response, exception)
)
.permitAll(); //以上提到的路径,都放行
}).authenticationManager(jsonAuthenticationManager());
1、完整的SecurityConfig
package com.javasm.securitydemo.common.config;
import com.alibaba.fastjson2.JSON;
import com.javasm.securitydemo.common.exception.R;
import com.javasm.securitydemo.login.json.JsonFilter;
import com.javasm.securitydemo.login.json.PhoneProvider;
import com.javasm.securitydemo.login.phone.PhoneFilter;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.context.SecurityContextHolderStrategy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
//****************JSON登录--开始****************//
@Resource(name = "usernameLoginUserDetailsService")
UserDetailsService userDetailsService;
@Bean
public DaoAuthenticationProvider jsonProvider() {
DaoAuthenticationProvider authenticationProvider =
new DaoAuthenticationProvider();
//配置 由哪个UserDetailsService负责查询用户信息UserDetails
authenticationProvider.setUserDetailsService(userDetailsService);
//配置密码的加密方式
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
//JSON等于专属的AuthenticationManager
@Bean("jsonAuthManager")
public AuthenticationManager jsonAuthenticationManager() {
return new ProviderManager(List.of(jsonProvider()));
}
//配置JsonFilter
public JsonFilter jsonFilter() {
JsonFilter jsonFilter = new JsonFilter();
//配置 AuthenticationManager
jsonFilter.setAuthenticationManager(jsonAuthenticationManager());
//json登录成功之后的显示
jsonFilter.setAuthenticationSuccessHandler(this::createSuccessJson);
//json登录失败之后,
jsonFilter.setAuthenticationFailureHandler(
(r, response, e) ->
createFailJson(response, e)
);
//配置json登录的路径
jsonFilter.setFilterProcessesUrl("/jsonlogin");
return jsonFilter;
}
//****************JSON登录--结束****************//
//****************Phone登录--开始****************//
@Resource
PhoneProvider phoneProvider;
//手机专属的 AuthenticationManger
@Bean("phoneAuthManager")
public AuthenticationManager phoneAuthManager() {
return new ProviderManager(List.of(phoneProvider));
}
@Autowired
public void configure(AuthenticationManagerBuilder auth) {
auth.authenticationProvider(phoneProvider);
}
@Bean
public PhoneFilter phoneFilter() {
PhoneFilter phoneFilter = new PhoneFilter();
//配置ProviderManager
phoneFilter.setAuthenticationManager(phoneAuthManager());
//json登录成功之后的显示
phoneFilter.setAuthenticationSuccessHandler(this::createSuccessJson);
//json登录失败之后,
phoneFilter.setAuthenticationFailureHandler(
(r, response, e) ->
createFailJson(response, e)
);
return phoneFilter;
}
//****************Phone登录--结束****************//
//****************跨域--开始****************//
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration corsConfiguration = new CorsConfiguration();
corsConfiguration.addAllowedOriginPattern("*");//允许所有的域名
corsConfiguration.addAllowedMethod("*");//允许所有的请求头
corsConfiguration.setAllowCredentials(true);//允许携带Cookie
corsConfiguration.setMaxAge(3600L);//预检测请求缓冲时间
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
//对所有的路径应用配置
source.registerCorsConfiguration("/**", corsConfiguration);
return source;
}
//****************跨域--结束****************//
@Bean
public SecurityContextRepository securityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.cors(cors -> cors.configurationSource(corsConfigurationSource()));
http.securityContext(context -> context.securityContextRepository(securityContextRepository()));
//因为当前json登录功能,和用户名密码登录功能类似,
// 所以把jsonfilter放到UsernamePasswordAuthenticaitonFilter相同的位置
http.addFilterAt(jsonFilter(), UsernamePasswordAuthenticationFilter.class);
http.addFilterAt(phoneFilter(), UsernamePasswordAuthenticationFilter.class);
//关闭csrf 跨域请求伪造的控制
http.csrf(csrf -> csrf.disable());
http.authorizeHttpRequests(
auth ->
auth.requestMatchers("/loginpage.html", "/login/**", "/jsonlogin", "/phoneLogin")
.permitAll()
.anyRequest().authenticated() //其他页面,要登录之后才能访问
);//放过登录接口,以及静态页面
// ↓配置表单提交
http.formLogin(form -> {
// 确保认证信息被正确设置并保存到session中
form.loginPage("/loginpage.html") //自定义登录页面的路径
.loginProcessingUrl("/javasmlogin") //表单提交的路径
.usernameParameter("uname") //自定义用户名的参数名(默认是username)
.passwordParameter("pwd")
//Authentication 是 UsernamePasswordAuthenticationToken-- principal实际的值,UserDetails
.successHandler(this::createSuccessJson)
//AuthenticationException 包含了 登录失败之后的 异常信息
.failureHandler((request, response, exception) ->
createFailJson(response, exception)
)
.permitAll(); //以上提到的路径,都放行
}).authenticationManager(jsonAuthenticationManager());
//注销登录
http.logout(logout -> logout
.logoutUrl("/logout")
//退出登录的时候,返回用户信息
.logoutSuccessHandler((r, response, a) -> createSuccessJson(response, "退出登录成功!"))
.permitAll()
);
//未登录异常提示
http.exceptionHandling().authenticationEntryPoint((request, response, e) ->
createFailJson(response, "当前用户未登录,请先登录再访问"));
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
//指定加密算法
return new BCryptPasswordEncoder();
}
private void createSuccessJson(HttpServletRequest request,HttpServletResponse response, Authentication authentication) {
SecurityContextHolderStrategy strategy = SecurityContextHolder.getContextHolderStrategy();
strategy.getContext().setAuthentication(authentication);
securityContextRepository().saveContext(strategy.getContext(), request, response);
createSuccessJson(response,authentication);
}
private void createSuccessJson(HttpServletResponse response, Object object) {
R ok = R.ok(object);
createJson(response, ok);
}
private void createFailJson(HttpServletResponse response, AuthenticationException e) {
R error = R.error(e.getMessage());
createJson(response, error);
}
private void createFailJson(HttpServletResponse response, String e) {
R error = R.error(e);
createJson(response, error);
}
private void createJson(HttpServletResponse response, R r) {
try {
response.setContentType("application/json;charset=utf-8");
//如果想修改返回的状态码,可以这么修改
//response.setStatus(r.getCode());
//写出去
PrintWriter writer = response.getWriter();
writer.write(JSON.toJSONString(r));
writer.flush();
writer.close();
} catch (IOException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
}
}
第二天总结
-
掌握Security的请求流程,能用语言描述
-
能够获取登录用户信息
-
修改登录用户信息
-
其他案例跟着课堂敲一遍,尽可能掌握,不强求