前言
在 Spring Boot + Vue 前后端分离项目中,后端只提供接口,页面处理和跳转都由前端实现,前后端通过 json 传输数据。
后端项目,搭建骨架,可以参考文章:使用 MybatisGenerator 根据数据库自动生成 model、mapper 接口和 mapper.xml
接下来开始后端登录接口的实现。
处理 User 用户类
让 User 类实现接口 UserDetails,并重写其中的方法:
public class User implements Serializable, UserDetails {
......
@Override
public boolean isAccountNonExpired() {
return true; // 账户没有过期
}
@Override
public boolean isAccountNonLocked() {
return true; // 账户没有锁定
}
@Override
public boolean isCredentialsNonExpired() {
return true; // 密码没有过期
}
@Override
public boolean isEnabled() {
return enabled; // 账户可以使用(需要删除 User 类自带的 getEnabled 方法,如果有的话)
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null; // 暂不设置
}
......
}
这里的 User 类,就是整个项目存放用户信息的类,有用户名、密码之类的。
UserService 配置
在包 service 下,新建一个 UserService 类,实现接口 UserDetailsService:
@Service
public class UserService implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userMapper.loadUserByUsername(username);
if (user == null){
throw new UsernameNotFoundException("用户名不存在!");
}
return user;
}
}
关键代码:
User user = userMapper.loadUserByUsername(username);
这里重写了 loadUserByUsername 方法,通过用户名加载用户信息。
UserMapper 配置
在接口 UserMapper 中,定义 loadUserByUsername 方法:
public interface UserMapper {
......
User loadUserByUsername(String username);
}
UserMapper.xml 配置
在 UserMapper.xml 实现方法 loadUserByUsername ,只需要添加一个查询语句:
......
<select id="loadUserByUsername" resultMap="BaseResultMap">
select * from user where username=#{username};
</select>
......
编写 RespBean 类
RespBean 类主要用于向前端返回数据,会在后端很多地方用到,比如 SpringSecurity 配置中就会用到:
public class RespBean {
private Integer status;
private String msg;
private Object obj;
public static RespBean ok(String msg){
return new RespBean(200, msg, null);
}
public static RespBean ok(String msg, Object obj){
return new RespBean(200, msg, obj);
}
public static RespBean error(String msg){
return new RespBean(500, msg, null);
}
public static RespBean error(String msg, Object obj){
return new RespBean(500, msg, obj);
}
private RespBean() {
}
private RespBean(Integer status, String msg, Object obj) {
this.status = status;
this.msg = msg;
this.obj = obj;
}
public Integer getStatus() {
return status;
}
public void setStatus(Integer status) {
this.status = status;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getObj() {
return obj;
}
public void setObj(Object obj) {
this.obj = obj;
}
}
注意一下,RespBean 类有两个构造函数,都是 private 修饰,表明其他的类就不能直接调用 RespBean 生成新的对象,这样,RespBean 类只有一个对象实例。
这里定义了两个静态方法 ok 和 error ,返回值都是 RespBean 对象。
Spring Security 配置
一般在 Spring Boot + Vue 前后端分离项目中,都是采用 Spring Security 作访问和权限控制。
关于 Spring Security 的权限控制暂且不谈,这里主要是登录接口的配置。
新建一个包 config,再建一个配置类 SecurityConfig ,继承 WebSecurityConfigurerAdapter :
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserService userService;
// 如果密码采用 BCryptPasswordEncoder 加密,则取消注释
// @Bean
// PasswordEncoder passwordEncoder(){
// return new BCryptPasswordEncoder();
// }
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/doLogin")
.loginPage("/login")
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
User user = (User) authentication.getPrincipal();
user.setPassword(null);
RespBean ok = RespBean.ok("登录成功!", user);
String string = new ObjectMapper().writeValueAsString(ok);
writer.write(string);
writer.flush();
writer.close();
}
})
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
RespBean respBean = RespBean.error("登录失败!");
if (httpServletResponse instanceof LockedException){
respBean.setMsg("账户被锁定,请联系管理员!");
}else if (httpServletResponse instanceof BadCredentialsException){
respBean.setMsg("用户名或密码输入错误!");
}else if (httpServletResponse instanceof DisabledException){
respBean.setMsg("账户被禁用,请联系管理员!");
}else if (httpServletResponse instanceof AccountExpiredException){
respBean.setMsg("账户过期,请联系管理员!");
}else if (httpServletResponse instanceof CredentialsExpiredException){
respBean.setMsg("密码过期,请联系管理员!");
}else {
respBean.setMsg("登录失败!");
}
String string = new ObjectMapper().writeValueAsString(respBean);
writer.write(string);
writer.flush();
writer.close();
}
})
.permitAll()
.and()
.logout()
.logoutSuccessHandler(new LogoutSuccessHandler() {
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
writer.write(new ObjectMapper().writeValueAsString(RespBean.ok("注销登录")));
writer.flush();
writer.close();
}
})
.permitAll()
.and()
.csrf().disable();
}
}
这个配置类比较长,我们挨个分析。
由于我这个数据库中 User 类的密码采用的是 md5DigestAsHex 加密,所以没有用 SpringSecurity 自带的加密方式 BCryptPasswordEncoder。
1、方法 configure(AuthenticationManagerBuilder auth) 解释
代码如下:
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userService);
}
这个方法身份验证,将登录的用户信息放在 auth 中,通过 userDetailsService 方法设置,传入的参数就是之前配置好的 userService。
2、方法 configure(HttpSecurity http) 解释
这个方法用于处理登录表单,主要分为三大块:
- 登录成功的配置
- 登录失败的配置
- 注销登录的配置
2.1 登录成功的配置
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().anyRequest().authenticated()
.and()
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/doLogin")
.loginPage("/login")
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
User user = (User) authentication.getPrincipal();
user.setPassword(null);
RespBean ok = RespBean.ok("登录成功!", user);
String string = new ObjectMapper().writeValueAsString(ok);
writer.write(string);
writer.flush();
writer.close();
}
})
......
}
其中
http.authorizeRequests().anyRequest().authenticated()
表明所有请求都需要登录过后才能访问。
.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginProcessingUrl("/doLogin")
.loginPage("/login")
配置登录表的 用户名、密码、处理登录的页面、登录成功的页面。
登录成功后:
httpServletResponse.setContentType("application/json;charset=utf-8");
PrintWriter writer = httpServletResponse.getWriter();
User user = (User) authentication.getPrincipal();
user.setPassword(null);
RespBean ok = RespBean.ok("登录成功!", user);
String string = new ObjectMapper().writeValueAsString(ok);
writer.write(string);
writer.flush();
writer.close();
这些配置几乎都是固定的,直接 copy 即可。
还有登录失败的配置、注销登录的配置,可以参考文章:Spring Security 基础教程 -- HttpSecurity 权限和登录表单配置
配置密码加密的类
由于我的数据库中 User 类的密码采用的是 md5DigestAsHex 加密,没有用 SpringSecurity 自带的加密方式 BCryptPasswordEncoder。
所以需要自定义加密类,照样在 config 包下,新建 MyPasswordEncoder 类:
@Component
public class MyPasswordEncoder implements PasswordEncoder {
@Override
public String encode(CharSequence charSequence) {
return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes());
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()));
}
}
配置登录成功后的页面
由于 SpringSecurity 在登录成功后,会自动跳转页面,而在 Spring Boot + Vue 前后端分离项目中,跳转页面是前端的事,后端只返回 json 数据即可。
所以需要额外配置一下。
在 controller 包下新建类 LoginController :
@RestController
public class LoginController {
@GetMapping("/login")
public RespBean login(){
return RespBean.error("尚未登录,请登录!");
}
}
测试登录接口
一切准备就绪,需要测试一下效果。
在 controller 包下新建类 HelloController:
@RestController
public class HelloController {
@GetMapping("/hello")
public String hello(){
return "hello";
}
}
这里定义了一个访问接口 /hello
,最后用 postman 测试。
postman 测试
- 启动项目,在 postman访问 http://localhost:8081/hello,效果如下:
- 登录操作
- 再访问接口
/hello
- 注销登录
注销登录是 get 请求,默认接口时 logout :
每天学习一点点,每天进步一点点。