前置知识:
后端: 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-parent2.6.3 org.springframework.boot spring-boot-starter-weborg.springframework.boot spring-boot-starter-testtest 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 MapuserMap = 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拦截下来.
以上两块因篇幅过长,另开文叙述,
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)