03 动力云客项目之登录功能后端实现

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的无非就是几样东西

  1. 实体类或vo类实现UserDetails接口
  2. service类实现UserDetailService接口并重写loadUserByUsername方法,以根据登录账号获得用户信息,确认用户是否存在
  3. 编写一个安全配置类与几个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 安全配置类
  1. 配置允许表单登录和登录的处理器
  2. 禁用跨站伪造请求管理 以便apifox等工具可以模拟请求
  3. 由于前后端项目启动端口不同, 允许跨域访问以便可以在浏览器直接访问后端信息
  4. 密码加密器
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 前后台联调

启动前端项目, 在浏览器中输入账号密码, 可以直接进入系统

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值