1 准备工作
1.1 创建项目
使用Spring initializr初始化项目
老师讲的是3.2.0, 但小版本之间问题应该不大.
1.2 项目结构
根据阿里巴巴Java开发手册确定项目结构
1.3 分层领域模型
【参考】分层领域模型规约:
• DO(Data Object):此对象与数据库表结构一一对应,通过 DAO 层向上传输数据源对象。
• DTO(Data Transfer Object):数据传输对象,Service 或 Manager 向外传输的对象。
• BO(Business Object):业务对象,可以由 Service 层输出的封装业务逻辑的对象。
• Query:数据查询对象,各层接收上层的查询请求。注意超过 2 个参数的查询封装,禁止使用 Map 类来传输。
• VO(View Object):显示层对象,通常是Web向模板渲染引擎层传输的对象。
我们只使用其中的三个,但在大型项目, DTO几乎是必须的:
1、DO(等价于我们的model)
2、Query (controller --> service -->manager --> dao 上一层往下一层传参数)
3、VO(等价于我们的result中的R对象)
即从dao一直到controller返回DAO, controller到前端返回R
1.4 前后端登录的步骤
1、vue前端通过axios提交loginAct、loginPwd参数;
2、后端UsernamePasswordAuthenticationFilter接收;
3、调用UserDetailsService的实现方法loadUserByUsername(String username)查询数据库;
4、返回UserDetails对象给Spring Security框架进行密码比较,比较通过,登录成功;
5、登录成功回调登录成功的handler,登录失败回调登录失败的handler;
6、前端获取后端返回的json结果:
结果response对象包含6个字段:
(1)config
(2)data 这个里面才是后端返回的数据,所以我们取该字段就行了;
(3)headers
(4)request
(5)status
(6)statusText
1.5 创建R处理结果集和响应码枚举类
package com.sunsplanter.result;
@Getter
@RequiredArgsConstructor
@NoArgsConstructor
@AllArgsConstructor
public enum CodeEnum {
OK(200, "成功"),
FAIL(500, "失败"),
TOKEN_IS_EMPTY(901, "请求Token参数为空"),
TOKEN_IS_ERROR(902, "请求Token有误"),
TOKEN_IS_EXPIRED(903, "请求Token已过期"),
TOKEN_IS_NONE_MATCH(904, "请求Token不匹配"),
USER_LOGOUT(200, "退出成功"),
DATA_ACCESS_EXCEPTION(500,"数据库操作失败"),
ACCESS_DENIED(500, "权限不足")
;
//结果码
private int code;
//结果信息
@NonNull
private String msg;
}
package com.sunsplanter.result;
/**
* 统一封装web层向前端页面返回的结果
*
*/
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class R {
//表示返回的结果码,比如200成功,500失败
private int code;
//表示返回的结果信息,比如 用户登录状态失效了,请求参数格式有误.......
private String msg;
//表示返回的结果数据,数据可能是一个对象,也可以是一个List集合.....
private Object data;
public static R OK() {
return R.builder()
.code(CodeEnum.OK.getCode())
.msg(CodeEnum.OK.getMsg())
.build();
}
public static R OK(int code, String msg) {
return R.builder()
.code(code)
.msg(msg)
.build();
}
public static R OK(Object data) {
return R.builder()
.code(CodeEnum.OK.getCode())
.msg(CodeEnum.OK.getMsg())
.data(data)
.build();
}
public static R OK(CodeEnum codeEnum) {
return R.builder()
.code(CodeEnum.OK.getCode())
.msg(codeEnum.getMsg())
.build();
}
public static R FAIL() {
return R.builder()
.code(CodeEnum.FAIL.getCode())
.msg(CodeEnum.FAIL.getMsg())
.build();
}
public static R FAIL(String msg) {
return R.builder()
.code(CodeEnum.FAIL.getCode())
.msg(msg)
.build();
}
public static R FAIL(CodeEnum codeEnum) {
return R.builder()
.code(codeEnum.getCode())
.msg(codeEnum.getMsg())
.build();
}
}
1.6 util包
1.6.1 JSONUtils负责对象与JSON的互相转化
package com.sunsplanter.config;
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.formLogin( (formLogin) -> {
formLogin.loginProcessingUrl("/api/login")//登录地址不需要写controller
.usernameParameter("loginAct")
.passwordParameter("loginPwd")
//给用户返回结果
.successHandler()
.failureHandler()
})
.authorizeHttpRequests( (authorize) -> {
authorize.anyRequest().authenticated();
})
.build();
}
}
1.6.2 ResponseUtils负责将JSON响应给前端
package com.sunsplanter.util;
public class ResponseUtils {
/**
* 使用response,把结果写出到前端
*
* @param response
* @param result
*/
public static void write(HttpServletResponse response, String result) {
response.setContentType("application/json;charset=UTF-8");
PrintWriter writer = null;
try {
writer = response.getWriter();
writer.write(result);
writer.flush();
} catch (IOException e) {
throw new RuntimeException(e);
} finally {
if (writer != null) {
writer.close();
}
}
}
}
2 . 安全相关
SS的无非就是几样东西
- 实体类或vo类实现UserDetails接口
- service类实现UserDetailService接口并重写loadUserByUsername方法,以根据登录账号获得用户信息,确认用户是否存在
- 编写一个安全配置类与几个handler
2.1 实现UserDetails接口
之前是单独创建一个VO类类实现UserDetails接口, 作为信息交互的中介
但实际上交互的对象就是表中的对象, 因此可以直接用实体类实现接口
在这里由于要做的是登录功能, 自然是根据用户对象进行登录 ,因此用TUser这个实体类实现UserDetails
package com.sunsplanter.model;
/**
* 用户表
*/
@AllArgsConstructor
@NoArgsConstructor
@Builder
@Data
public class TUser implements UserDetails,Serializable {
/**
* 主键,自动增长,用户ID
*/
private Integer id;
/**
* 登录账号
*/
private String loginAct;
/**
* 登录密码
*/
private String loginPwd;
/**
* 用户姓名
*/
private String name;
/**
* 用户手机
*/
private String phone;
/**
* 用户邮箱
*/
private String email;
/**
* 账户是否没有过期,0已过期 1正常
*/
private Integer accountNoExpired;
/**
* 密码是否没有过期,0已过期 1正常
*/
private Integer credentialsNoExpired;
/**
* 账号是否没有锁定,0已锁定 1正常
*/
private Integer accountNoLocked;
/**
* 账号是否启用,0禁用 1启用
*/
private Integer accountEnabled;
/**
* 创建时间
*/
private Date createTime;
/**
* 创建人
*/
private Integer createBy;
/**
* 编辑时间
*/
private Date editTime;
/**
* 编辑人
*/
private Integer editBy;
/**
* 最近登录时间
*/
private Date lastLoginTime;
/*
*以下两个集合不存储在数据库中, 只有role,permission才存储在数据库中
* 但一个用户可能拥有多个role,permission, 所以自定义一个集合来当中介
* 角色list,权限list
*/
private List<String> roleList;
private List<String> permissionList;
/*
* 以下为实现UserDetails而重写的七个方法
*/
//避免该方法的返回值被序列化
@JsonIgnore
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> list = new ArrayList<>();
//分别将数据库取到的角色和权限放进list
this.getRoleList().forEach(role -> {
list.add(new SimpleGrantedAuthority(role));
});
this.getPermissionList().forEach(permission -> {
list.add(new SimpleGrantedAuthority(permission));
});
return list;
}
@JsonIgnore
@Override
public String getPassword() {
return this.getLoginPwd();
}
@JsonIgnore
@Override
public String getUsername() {
return this.getLoginAct();
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
//等于1就是没有过期
return this.getAccountNoExpired() == 1;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return this.getAccountNoLocked() == 1;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return this.getCredentialsNoExpired() == 1;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return this.getAccountEnabled() == 1;
}
}
2.2 service类实现UserDetailService接口并重写loadUserByUsername方法,以根据登录账号获得用户信息,确认用户是否存在
2.2.1 实现UserDetailService接口
具体来说
先用UserService继承UserDetailsService接口
再用 UserServiceImpl实现 UserService接口, 以此实现UserDetailService接口
package com.sunsplanter.service;
public interface UserService extends UserDetailsService {
}
package com.sunsplanter.service.impl;
@Service
public class UserServiceImpl implements UserService {
@Resource
private TUserMapper tUserMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
TUser tUser = tUserMapper.selectByLoginAct(username);
if (tUser == null)
throw new UsernameNotFoundException("登录账号不存在");
return tUser;
}
}
2.2.2 编写SQL语句
MB并不包含一些单表的简单方法, 因此基本都需要自己写
在TUserMapper接口中新增一个方法
TUser selectByLoginAct(String username);
在 TUserMapper.xml中写具体SQL代码
<select id="selectByLoginAct" parameterType="java.lang.String" resultMap="BaseResultMap">
select
<include refid="Base_Column_List" />
from dlyk.t_user
where login_act = #{username}
</select>
2.2.3 编写SecurityConfig配置类和四种登录情况处理器
2.2.3.1 四种登录情况处理器
package com.sunsplanter.config.handler;
import java.io.IOException;
//没有权限登录时会自动执行这个类中的handler方法, 该方法返回自定义的Json给前端
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//没有权限访问,执行该方法,在该方法中返回json给前端,就行了
//登录失败的统一结果
R result = R.FAIL(CodeEnum.ACCESS_DENIED);
//把R对象转成json
String resultJSON = JSONUtils.toJSON(result);
//把R以json返回给前端
ResponseUtils.write(response, resultJSON);
}
}
package com.sunsplanter.config.handler;
import java.io.IOException;
//登录失败会自动执行这个类中的onAuthenticationFailure方法, 该方法返回自定义的Json给前端
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
//
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//登录失败的统一结果
R fail = R.FAIL(exception.getMessage());
//将对象序列化为Json
String resultJSON = JSONUtils.toJSON(fail);
//把R以Json的形式返回给前端
ResponseUtils.write(response, resultJSON);
}
}
package com.sunsplanter.config.handler;
登录成功会自动执行这个类中的onAuthenticationSuccess方法, 该方法返回自定义的Json给前端
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//登录成功,执行该方法,在该方法中返回json给前端,就行了
TUser tUser = (TUser) authentication.getPrincipal();
//登录成功的统一结果
R result = R.OK(tUser);
//把R对象转成json
String resultJSON = JSONUtils.toJSON(result);
//把R以json返回给前端
ResponseUtils.write(response, resultJSON);
}
}
package com.sunsplanter.config.handler;
//成功登出会自动执行这个类中的方法, 该方法返回自定义的Json给前端
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler{
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//退出成功,执行该方法,在该方法中返回json给前端,就行了
TUser tUser = (TUser)authentication.getPrincipal();
//退出成功的统一结果
R result = R.OK(CodeEnum.USER_LOGOUT);
//把R对象转成json
String resultJSON = JSONUtils.toJSON(result);
//把R以json返回给前端
ResponseUtils.write(response, resultJSON);
}
}
2.3.2.2 安全配置类
- 配置允许表单登录和登录的处理器
- 禁用跨站伪造请求管理 以便apifox等工具可以模拟请求
- 由于前后端项目启动端口不同, 允许跨域访问以便可以在浏览器直接访问后端信息
- 密码加密器
package com.sunsplanter.config;
@Configuration
public class SecurityConfig {
@Resource
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Resource
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Resource
private MyLogoutSuccessHandler myLogoutSuccessHandler;
@Resource
private MyAccessDeniedHandler myAccessDeniedHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.formLogin( (formLogin) -> {
formLogin.loginProcessingUrl("/api/login")//登录地址不需要写controller
.usernameParameter("loginAct")
.passwordParameter("loginPwd")
//给用户返回结果
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler);
})
.authorizeHttpRequests( (authorize) -> {
//对于等于页面,允许所有人访问
authorize.requestMatchers("api/login").permitAll()
.anyRequest().authenticated();
})
//禁用跨站请求伪造,作用与@CrossOrigin类似
.csrf((csrf) -> {
csrf.disable();
})
//支持跨域请求.
//前端启动端口是8081, 后端启动端口是8089,协议/域名/端口任一不同都属于跨域.会被自动阻断
//以支持前后端调试
.cors( (cors) -> {
cors.configurationSource(configurationSource);
})
.build();
}
//配置跨域Bean对象,以便打通并配置前端8081和后端8089.并设置后端8089可以允许的访问来源
@Bean
public CorsConfigurationSource configurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*")); //允许任何来源,http://localhost:8081
configuration.setAllowedMethods(Arrays.asList("*")); //允许任何请求方法,post、get、put、delete
configuration.setAllowedHeaders(Arrays.asList("*")); //允许任何的请求头
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean //There is no PasswordEncoder mapped for the id "null"
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
2.3 测试后台可以正常接收并返回参数
2.4 前后台联调
启动前端项目, 在浏览器中输入账号密码, 可以直接进入系统