ruoyi-vue第一章 : 学习后台登录模块(SpringSecurity)

ruoyi-vue第一章 : 学习后台登录模块(SpringSecurity),第1张

ruoyi-vue第一章 : 学习后台登录模块(SpringSecurity) ruoyi-vue第一章 : 学习后台登录模块(SpringSecurity)

前置知识:

后端: SpringBoot + mybatis + maven.前端(认识就行): vue + js.

如果没有对应的知识, 建议学习后再来观看本视频.

1. ruoyi-vue项目介绍及登录模块梳理

ruoyi是gitee上顶流的开源项目之一, 常被作为外包,基础开发等开发的基础项目, 开箱即用, 并且提供了一体版本, 前后分离版本, 分布式版本, 其他衍生版本.

本系列教程以前后端分离版本(ruoyi-vue)为主.

项目地址: https://gitee.com/y_project/RuoYi-Vue?_from=gitee_search

演示地址:http://vue.ruoyi.vip/login?redirect=%2Findex
文档地址:http://doc.ruoyi.vip

我们点击演示地址,

本次要实现的就是该页面的后端, 除了验证码和token权限相关的内容(避免篇幅过长).

ruoyi源码现如今已经较庞大, 我这里会先行阅读并尽可能抽离单独的模块进行笔记.

如果你有兴趣, 可以跟进后续其他模块.

2. SpringSecurity 框架介绍

Spring 是非常流行和成功的 Java 应用开发框架,SpringSecurity 正是 Spring 家族中的成员。SpringSecurity 基于 Spring 框架,提供了一套 Web 应用安全性的完整解决方案。

正如你可能知道的关于安全方面的两个主要区域是“认证”和“授权”(或者访问控制),一般来说,Web 应用的安全性包括用户认证(Authentication)和用户授权(Authorization)两个部分,这两点也是 Spring Security 重要核心功能。

(1)用户认证指的是:验证某个用户是否为系统中的合法主体,也就是说用户能否访问该系统。用户认证一般要求用户提供用户名和密码。系统通过校验用户名和密码来完成认证过程。通俗点说就是系统认为用户是否能登录

(2)用户授权指的是验证某个用户是否有权限执行某个 *** 作。在一个系统中,不同用户所具有的权限是不同的。比如对一个文件来说,有的用户只能进行读取,而有的用户可以进行修改。一般来说,系统会为不同的用户分配不同的角色,而每个角色则对应一系列的权限。通俗点讲就是系统判断用户是否有权限去做某些事情。

Spring Security 是 Spring家族中的一个安全管理框架,实际上,在 Spring Boot 出现之前,Spring Security 就已经发展了多年了,但是使用的并不多,安全管理这个领域,一直是 Shiro 的天下。

相对于 Shiro,在 SSM 中整合Spring Security 都是比较麻烦的 *** 作,所以,SpringSecurity 虽然功能比Shiro 强大,但是使用反而没有 Shiro 多(Shiro 虽然功能没有Spring Security 多,但是对于大部分项目而言,Shiro 也够用了)。

自从有了 Spring Boot 之后,Spring Boot 对于 Spring Security 提供了自动化配置方案,可以使用更少的配置来使用 SpringSecurity。因此,一般来说,常见的安全管理技术栈的组合是这样的:

SSM + ShiroSpring Boot/Spring Cloud + SpringSecurity 3. SpringSecurity的第一个项目

我们这里使用IDEA编译器新建SpringBoot项目, 随后整合进SpringSecurity框架, 来让大家直观感受一下.

3.1 使用IDEA新建一个SpringBoot项目.

新建一个空的maven项目, 然后maven引入SpringBoot 和SpringBoot-web/test的依赖就行了.

    
        org.springframework.boot
        spring-boot-starter-parent
        2.6.3
         
    

        
            org.springframework.boot
            spring-boot-starter-web
        

        
            org.springframework.boot
            spring-boot-starter-test
            test
        
    

    
        
            
                org.springframework.boot
                spring-boot-maven-plugin
            
        
    

添加base包扫描注解

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
// 添加自动配置和base包扫描注解
@EnableConfigurationProperties
@SpringBootApplication(scanbasePackages = { "com.security.demo"})
public class DemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}

新建一个controller的包, 写一个HelloController的类

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
// 默认不写url, 即"/"
@RestController
public class HelloController {

    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }
}

yml中指定端口:8080

server:
  port: 8080

运行测试一下
网页中显示Hello, 说明SpringBoot项目搭建完毕.

3.2 添加SpringSecurity 框架依赖

在之前我们的SpringBoot项目中的pom文件中添加一个SpringSecurity的依赖就好,

        
            org.springframework.boot
            spring-boot-starter-security
        

然后再次运行项目, 在浏览器输入

localhost:8080/hello

会发现跳转到登录页面

http://localhost:8080/login

那么账号密码在哪里呢?

账号是默认的"user"

密码则是随机生成的, 打开控制台, 我们可以看到系统生成的一段密码

3.3 配置默认账号密码

接下来, 我们简单的配置一下默认的账号密码(不连接数据库).

3.3.1 使用yml配置账号密码

在yml中添加以下代码进入文件底部, 注意需要保持格式.

# 第一行从行首开始, 前面不要有多余的空格.
spring:
  security:
    user:
	  #自定义账号密码都为"admin"
      name: admin
      password: admin

然后输入测试. ok~

3.3.2 使用java代码方式配置

使用java方式前, 请先删除掉yml中的配置

新建一个config包, 然后新建一个类SpringSecurityConfig

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在内存中添加默认的账号密码及权限(必须要有权限, 不然会报错), 之后会涉及权限知识, 此处不涉及.
        auth.inMemoryAuthentication()
            // username : admin1
            // password : 123456
                .withUser("admin1").password("123456").roles("admin")
                .and()
                .withUser("admin2").password("654321").roles("user");
    }
}

然后测试ok~.

这样就算完成了一个入门demo了.不过问题还有很多 :

    没有连数据库登陆页面是系统提供的, 无法满足甲方.无法登出.密码没有加密.
4. 对入门案例进行改进 4.1 自定义登录页面

首先, 我们需要引入thymeleaf相关依赖, 这样就能愉快的写前端页面.

在pom中引入

 		
        
            org.springframework.boot
            spring-boot-starter-thymeleaf
        

在resources下新建templates目录(名字绝对不能错, 不然thymeleaf引擎认不出来), 然后在templates目录下新建login.html

内容如下:




    
    第一个HTML页面



    
    Title


自定义表单验证:




然后加一个页面跳转类

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

// 这里必须是Controller, 不然没法跳页面
@Controller
public class PageController {
    @RequestMapping("/loginPage")
    public String login() {
        // 输入loginPage会跳转到login.html页面
        return "login";
    }
}

这样我们的登录页面就写好了, 但是注意 : Security不知道我们的登录页面, 所以我们现在去访问登陆页面, 会被转到Security的登陆页面.

所以我们打开我们自定义的SecurityConfig类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;


@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {

    
    @Bean
    PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }

    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 在内存中添加默认的账号密码及权限(必须要有权限, 不然会报错).
        auth.inMemoryAuthentication()
                .withUser("admin1").password("123456").roles("admin")
                .and()
                .withUser("admin2").password("654321").roles("user");
    }
	
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 允许任何人访问loginPage, Security 不保护这两个资源.
        http.authorizeRequests().antMatchers("/loginPage", "login.html")
                .permitAll().anyRequest().authenticated()
                .and()
                // 让security认准自定义的登录页
                .formLogin().loginPage("/loginPage")
                // 登录 *** 作
                .loginProcessingUrl("/authentication/form")
                // 失败后跳转
                .failureUrl("/login?error")
                // 成功后
                .defaultSuccessUrl("/hello")
                // username和password的参数名, 与html中的name一致
                .usernameParameter("username")
                .passwordParameter("password")
                .permitAll();
        // 如果自定义login页面, 需要禁用csrf验证
        // 如果不禁用, security会认为我们自定义的登录 *** 作是非法入侵.
        http.csrf().disable();
    }
}

然后再启动项目, ok~

4.2 连接数据库

这里因为链接数据库需要引入mybatis, 还需要新建数据表, 一套讲完就太浪费时间了. 所以我们在Service层使用Map结构来模拟数据库 *** 作就可以了.

先定义entity包下的User类, 注意 UserDetails接口下的boolean方法, 都需要改为true, 不然登录会失败.

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class User implements UserDetails {
    private String username;
    private String password;

    // getter和setter. 这里要求必须是getUserName和getPassword, 因为UserDetails接口中有强制定义.
    
    
    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    
    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    
    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    
    @Override
    public boolean isEnabled() {
        return true;
    }

    @Override
    public Collection getAuthorities() {
        return null;
    }
}

然后定义一个UserServiceDetailImpl, 需要实现UserDetailsService 接口, 这是Security强制要求的.

import com.security.demo.demo.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

@Service
public class UserServiceDetailImpl implements UserDetailsService {
    // 这是个数据库
    private Map userMap = new HashMap<>();

    
    public User getUserByUsername(String username) {
        initDataCollection();
        return userMap.get(username);
    }

    private void initDataCollection() {
        User user = new User();
        user.setUsername("testMysql");
        user.setPassword("12345");
        userMap.put("testMysql", user);
    }
    
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = getUserByUsername(username);
        return user;
    }
   
}

接下来, 我们开始改造SpringSecurityConfig. 目的就是告知他, 我们现在有了自己的UserService, 下次有用户登录, 你就用我得这个userService获取.

	@Autowired
    UserServiceDetailImpl userServiceDetail;
    
    //在这里完成获得数据库中的用户信息
    //密码一定要加密
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userServiceDetail);
    }

如果对Map存储心有不满, 可以自己加上数据库相关依赖后更新Service即可.

4.3 加密密码

前面我们使用的密码都是明文的,这是非常不安全的。一般情况下用户的密码需要进行加密后再保存到数据库中。

常见的密码加密方式有:
3DES、AES、DES:使用对称加密算法,可以通过解密来还原出原始密码
MD5、SHA1:使用单向HASH算法,无法通过计算还原出原始密码,但是可以建立彩虹表进行查表破解
bcrypt:将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题
																					---摘自网络

首先, 先去更新配置类SpringSecurityConfig

    
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userServiceDetail)
            // 加一个密码编码器
            .passwordEncoder(passwordEncoder());
    }

然后我们更新下数据库中的密码

    private void initDataCollection() {
        User user = new User();
        user.setUsername("testMysql");
        // 先加密再存储.
        user.setPassword(new BCryptPasswordEncoder().encode("11111"));
        userMap.put("testMysql", user);
    }
4.4 退出登录

退出登录,

protected void configure(HttpSecurity http) throws Exception {
        // ... 之前写的代码
        
        // 添加logout配置, 这样用户输入localhost:8080/logout就会退出登录状态, 
        // 然后跳转到localhost:8080/login?logout
        http.logout().logoutUrl("/logout").logoutSuccessUrl("/login?logout");
}

html我们不改造了, 直接再登录后,url输入一下localhost:8080/logout, 然后重试localhost:8080/hello, 被阻拦到login页即可

4.5 SpringSecurity小结

在入门案例里, 来来回回就是和

class SpringSecurityConfig extends WebSecurityConfigurerAdapter

这个类打招呼.

先是配置账号密码, 然后配置登录页面,

想要配置userService, 还得实现UserDetailService.

而且加密密码和退出登录必须按照他的配置要求去做.

5. 前后端分离的登录页面 5.1 下载并启动ruoyi-ui项目

接着我们打开ruoyi-ui项目, 这里他是前后端分离的, 我们按照他的思路

先更改vue.config.js中的process.env.VUE_APP_base_API.target

[process.env.VUE_APP_base_API]: {
    // 主要就是为了保证这里的端口和URL与后端一致
        target: `http://localhost:8080`,
        changeOrigin: true,
        pathRewrite: {
          ['^' + process.env.VUE_APP_base_API]: ''
        }

直接按照readme.md去运行. vue运行的项目就是本地80端口, 所以网页里输入就可以看到登录页了.

5.2 做基本的工具包准备

新建一个AjaxResult, 作为前后端交互的基本对象, 这里我们直接按照ruoyi的来写了.

正常开发里, 我们也是从别的项目直接copy或者通过jar的形式引入工具包就行, 所以不必太在意.

package com.security.demo.demo.domain;

import com.security.demo.demo.utils.HttpStatus;
import com.security.demo.demo.utils.StringUtils;

import java.util.HashMap;


public class AjaxResult extends HashMap {
    private static final long serialVersionUID = 1L;

    
    public static final String CODE_TAG = "code";

    
    public static final String MSG_TAG = "msg";

    
    public static final String DATA_TAG = "data";

    
    public AjaxResult() {
    }

    
    public AjaxResult(int code, String msg) {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
    }

    
    public AjaxResult(int code, String msg, Object data) {
        super.put(CODE_TAG, code);
        super.put(MSG_TAG, msg);
        if (StringUtils.isNotNull(data)) {
            super.put(DATA_TAG, data);
        }
    }

    
    public static AjaxResult success() {
        return AjaxResult.success(" *** 作成功");
    }

    
    public static AjaxResult success(Object data) {
        return AjaxResult.success(" *** 作成功", data);
    }

    
    public static AjaxResult success(String msg) {
        return AjaxResult.success(msg, null);
    }

    
    public static AjaxResult success(String msg, Object data) {
        return new AjaxResult(HttpStatus.SUCCESS, msg, data);
    }

    
    public static AjaxResult error() {
        return AjaxResult.error(" *** 作失败");
    }

    
    public static AjaxResult error(String msg) {
        return AjaxResult.error(msg, null);
    }

    
    public static AjaxResult error(String msg, Object data) {
        return new AjaxResult(HttpStatus.ERROR, msg, data);
    }

    
    public static AjaxResult error(int code, String msg) {
        return new AjaxResult(code, msg, null);
    }
}

抄完之后会报错, 我们需要另外两个工具类, 一个是HttpStatus. 也是照抄就行

package com.security.demo.demo.utils;


public class HttpStatus {
    
    public static final int SUCCESS = 200;

    
    public static final int CREATED = 201;

    
    public static final int ACCEPTED = 202;

    
    public static final int NO_ConTENT = 204;

    
    public static final int MOVED_PERM = 301;

    
    public static final int SEE_OTHER = 303;

    
    public static final int NOT_MODIFIED = 304;

    
    public static final int BAD_REQUEST = 400;

    
    public static final int UNAUTHORIZED = 401;

    
    public static final int FORBIDDEN = 403;

    
    public static final int NOT_FOUND = 404;

    
    public static final int BAD_METHOD = 405;

    
    public static final int ConFLICT = 409;

    
    public static final int UNSUPPORTED_TYPE = 415;

    
    public static final int ERROR = 500;

    
    public static final int NOT_IMPLEMENTED = 501;
}

字符串工具类

package com.security.demo.demo.utils;

public class StringUtils {
    
    public static boolean isNull(Object object) {
        return object == null;
    }

    
    public static boolean isNotNull(Object object) {
        return !isNull(object);
    }
}

5.3 关闭掉ruoyi的验证码, 简化

然后打开F12, 我们可以看到他这里发了一个请求, 是请求验证码的

我们先不处理这个验证码, 所以直接新建一个controller, 关闭验证码即可, 这里我是参考ruoyi的后端代码直接写的, 大家开箱即用即可.

import com.security.demo.demo.domain.AjaxResult;
import com.security.demo.demo.entity.LoginBody;
import com.security.demo.demo.service.LoginService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

    @RequestMapping("captchaImage")
    public AjaxResult captchaImage() {
        AjaxResult ajaxResult = AjaxResult.success();
        // 不输入验证码
        ajaxResult.put("captchaOnOff", false);
        return ajaxResult;
    }
}

这时候还没完成, 我们security还在保护这个接口, 我们去config类里改一下.

我们去config类, 改动一下

这里我们放行captchaImage接口, 无需登录即可访问.

然后启动后台, F5刷新登录页即可看到验证码消失了.

5.4 login *** 作实现

我们还是打开F12, 点一下登陆看一下, 发现VUE会发出请求如下:

请求类型: POST请求
// dev-api是vue处于开发环境的, 他会映射到/login中, 所以不用在意.
URL : http://localhost/dev-api/login
Json 参数 : {
// 这里是我在登录框输入的内容
	"password": "admin",
	"username": "123456"
}

所以我们更新下HelloController, 按照http请求实现一下login方法.

	// LoginService实现在下面一个代码块
	@Autowired
    LoginService loginService;
	// login登录成功后会返回前端一个token作为用户的标识. 这里我们先实现一个伪token逻辑, 方便后续更新.
    @PostMapping("login")
    public AjaxResult login(@RequestBody LoginBody loginBody) {
        // 在这里只处理接受前端的login请求, 具体的处理判断, 我们仍交给Security去做
        AjaxResult ajax;
        String token = loginService.login(loginBody.getUsername(), loginBody.getPassword());
        if (token == null || token.equals("")) {
            // 登录失败则返回空字串, 所以直接error, 让前端打印一下就行.
            ajax = AjaxResult.error("密码错误");
        } else {
            // 账号密码正确则加入token
            ajax = AjaxResult.success();
            ajax.put("token", token);
        }
        return ajax;
    }
package com.security.demo.demo.service;

import com.security.demo.demo.entity.User;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class LoginService {
    
    @Resource
    private AuthenticationManager authenticationManager;

    
    public String login(String username, String password) {
        // 用户验证
        Authentication authentication = null;
        try {
            // 该
            方法会去调用UserDetailsServiceImpl.loadUserByUsername() 交给他去校验账号密码.
            authentication = authenticationManager
                    .authenticate(new UsernamePasswordAuthenticationToken(username, password));
        } catch (Exception e) {
            // 如果用户密码错误, 那么会抛出new BadCredentialsException()异常
            e.printStackTrace();
            // 返回一个空的token
            return "";
        }
        User loginUser = (User) authentication.getPrincipal();
        // 你有了user, 就可以生成token
        return createToken(loginUser);
    }
    
	// 之后我们再完善createToken即可, 目前只做伪逻辑.
    private String createToken(User loginUser) {
        return loginUser.toString();
    }
}

在我们的SecurityConfig类里,我们追加一个注入器. 让Spring管理这个登陆账号密码比对器.

	
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

如果你前面已经将/login加入了非保护名单, 那么就直接启动项目, 没有的话, 去加一下.

注意一下, 测试的时候, 如果你看到这样, 就说明前端联通了后端,. 但是密码错了, 你可以试试debug跟一下LoginService的逻辑.

然后我这里输入正确的账号密码(在Map中自定义的testMysql用户)

响应:

这样就登录成功了, 登陆成功后因为我们的是伪token, 如果需要测试, 需要去存储里清楚缓存即可.

可以清缓存后,多次实验.

6. 拓展知识
    验证码模块

    手机验证码邮箱验证码图片验证码(ruoyi采用的方案, 另开文讲解.) token和权限模块

    获得token后, 去得到用户相关信息(getInfo)每个用户因其角色不同, 应该让用户只看到自己可 *** 作的相关界面就算用户请求发到后台, 也应该直接交给SpringSecurity拦截下来.

以上两块因篇幅过长,另开文叙述,

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/zaji/5719525.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-18
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存