在上节中实现了SpringBoot+JWT登录,但是介绍的登录是基于SpringSecurity的默认登录页实现的。但是项目开发目前很多都是前后端分离的,也就是VUE+API接口的模式。所以我们需要实现在API接口中使用SpringSecurity登录。
首先需要在WebSecurityConfig中增加AuthenticationManager,以便API接口类中能够引用这个AuthenticationManager:
//作用:暴露AuthenticationManager给其他Bean使用@Bean@Overrideprotected AuthenticationManager authenticationManager() throws Exception {return super.authenticationManager();//return super.authenticationManagerBean();}
如果不加这个方法的话,在API 中无法通过@AutoWired引用AuthenticationManager。
下面我们再开发一个API接口类JwtLoginDemoApi(openjweb-sys工程里):
package org.openjweb.sys.api;import lombok.extern.slf4j.Slf4j;
import org.openjweb.core.entity.CommUser;
import org.openjweb.core.service.CommUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;/*** 测试URL:http://localhost:8001/demo/jwt/login?loginId=admin&password=Hello0214@*/
@RestController
@RequestMapping("/demo/jwt")
@Slf4j
public class JwtLoginDemoApi {@Autowiredprivate AuthenticationManager authenticationManager; //WebSecurityConfig声明以后这里就不报红了@AutowiredCommUserService sysUserService;@RequestMapping("login")public String login(String loginId,String password){CommUser sysUser = sysUserService.selectUserByLoginId(loginId);//UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(sysUser,password);// 生成一个包含账号密码的认证信息Authentication token = new UsernamePasswordAuthenticationToken(loginId,password);Authentication authentication = authenticationManager.authenticate(token);// 将返回的Authentication存到上下文中SecurityContextHolder.getContext().setAuthentication(authentication);// CommUser user = (CommUser) authentication.getPrincipal();log.info("账号:"+user.getLoginId());return "登录成功,登录账号为:"+user.getLoginId();}
}
启动SpringBoot,然后访问
http://localhost:8001/demo/jwt/login?loginId=admin&password=Hello0214@
界面显示:
在上面的程序代码中,认证成功后,通过CommUser user = (CommUser) authentication.getPrincipal();获取用户信息(CommUser是实现了UserDetails接口)。登录后,控制台显示的信息:
看控制台的信息,在登录的时候调用了JwtAuthenticationFilter,就是上节中介绍的过滤器,但是登录成功后,并没有执行LoginSuccessHandler,我们希望在登录成功后能够通过LoginSuccessHandler加上JWT的accessToken。目前暂时没有还没找到办法能够像表单登录自动调用WebSecurityConfig中 configure(HttpSecurity http)里的各种设置,不过可以增加下面的代码调用LoginSuccessHandler:
@AutowiredLoginSuccessHandler loginSuccessHandler;......ServletRequestAttributes sra = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();loginSuccessHandler.onAuthenticationSuccess(sra.getRequest(),sra.getResponse(),authentication);return "登录成功,登录账号为:"+user.getLoginId();......
上面的代码就是在增加LoginSuccesshandler组件,然后认证成功后,将认证成功后的authentication交给loginSuccessHandler执行JWT封装处理,这样登录成功后,界面返回的就是上节的登录成功的JSON:{"msg":"操作成功","code":0,"data":"SuccessLogin"}
如果认证失败,会返回到登录页面,现在模拟密码错误的请求:
http://localhost:8001/demo/jwt/login?loginId=admin&password=1234,界面会跳转到登录界面:
对于前后端分离的模式,登录失败应该返回JSON接口,显然上面返回一个登录界面不是我们想要的。所以我们需要进行改造。经测试,
Authentication authentication = authenticationManager.authenticate(token);
如果认证失败,这行代码下面的代码不会执行,而是被SpringSecurity跳转了。我们再打开WebSecurityConfig.java,找到
//.authenticationEntryPoint(jwtAuthenticationEntryPoint)
去掉注释,然后在测下失败登录,界面显示{"msg":"请先登录","code":-3,"data":{}},这样就达到返回JSON错误消息的效果了。
需要说明的是,这个EntryPoint打开后,localhost:8001/login就不管用了,显示下面的页面:
不过因为我们反正是做前后端分离模式的开发,所以不能用反而是我们需要的。因为生产环境不会使用localhost:8001/login。不过为什么会这样后面还需要花时间研究。
现在我们发现,当使用API接口调用登录时,WebSecurityConfig中配置是有的有效(如JwtAuthenticationFilter、还有上面的authenticationEntryPoint),但个别配置没起作用(LoginSuccessHandler),这个以后再研究。
【自定义AuthenticationProvider】
现在介绍下如何开发自定义的AuthenticationProvider,在实际生产环境中,可能在登录的时候还需要做很多各种其他的验证,比如手机号验证,安全验证等,所以可能需要自定义AuthenticationProvider。
我们在openjweb-sys下创建一个MyAuthenticationProvider:
package org.openjweb.sys.provider;import lombok.extern.slf4j.Slf4j;
import org.openjweb.common.util.AESUtil;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;import java.util.Collections;@Slf4j
public class MyAuthenticationProvider implements AuthenticationProvider {private UserDetailsService userDetailsService;public MyAuthenticationProvider(UserDetailsService userDetailsService) {this.userDetailsService = userDetailsService;}@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {log.info("自定义的AuthenticationProvider..................");String username = authentication.getName();String password = authentication.getCredentials().toString();UserDetails userDetails = userDetailsService.loadUserByUsername(username);//自行密码验证//将数据库中的AES密码还原String encodePwd = userDetails.getPassword();//这个KEY怎么传进来?暂时先写死String decodePwd = AESUtil.aesDncode("/Z3E1YW1mxM0BCluJdYaLHCnhTuzE8j0",encodePwd);if (userDetails == null || !password.equals(decodePwd)) {log.info("抛出异常.................");throw new BadCredentialsException("Invalid username or password");}return new UsernamePasswordAuthenticationToken(userDetails, password, Collections.emptyList());}@Overridepublic boolean supports(Class<?> authentication) {return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);}
}
在上面的代码中,因为数据库中存储的密码是AES加密的,所以在密码比对的时候,将数据库的AES密码还原后,与前端用户传入的密码进行比较,当然可以将前端用户的密码用AES加密和数据库中加密的密码比较,另外AES加解密需要的key还没考虑如何传进来,先在代码里写死,以后再完善。
定义了自定义的AuthenticationProvider后,需要在WebSecurityConfig中做下配置:
@Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {if(false){//如果自定义AuthenticationProvider 则不使用这个auth.userDetailsService(userDetailService).passwordEncoder(aesPasswordEncoder);//auth.userDetailsService(userDetailService).passwordEncoder(new BCryptPasswordEncoder());}else{//自定义AuthenticationProviderauth.authenticationProvider(new MyAuthenticationProvider(userDetailService));}}
上面代码中,因为使用if(false),所以执行的是自定义的Provider,如果不使用自定义的就把false改为true。
现在我们再启动SpringBoot访问测试地址,会发现可以成功登录了。
本文总结:
本文介绍了在API接口类中实现了SpringSecurity的用户登录,这种情况更符合现在的前后端分离的开发模式,另外介绍了认证成功后如何跳转到loginSuccessHandler从而自动进行JWT生成accessToken,以及认证失败如何返回失败的JSON,另外介绍了自定义AuthenticationProvider的开发。相信本文对做SpringSecurity API 登录的朋友能有不小的帮助。完整代码可以从Github上下载(如果大家觉得有帮助,可在GitHub上点个Star,感谢!)。
GitHub - openjweb/cloud at masterOpenJWeb is a java bases low code platform. Contribute to openjweb/cloud development by creating an account on GitHub.https://github.com/openjweb/cloud/tree/master