前置知识:
后端: 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项目搭建完毕.
在之前我们的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了.不过问题还有很多 :
- 没有连数据库登陆页面是系统提供的, 无法满足甲方.无法登出.密码没有加密.
首先, 我们需要引入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 extends GrantedAuthority> 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刷新登录页即可看到验证码消失了.
我们还是打开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拦截下来.
以上两块因篇幅过长,另开文叙述,



