SpringSecurity 入门
一步一步来。
集各家之所长,师从 尚硅谷、狂神、三更草堂;如果文中发现跟他们有类似的,不用想,就是他们那里拿来的
事先声明,本人很菜,其中有说的误人子弟的,请大家指出来。
环境:
idea2020.1 SpringBoot2.6.3 MyBatisPlus Maven3.6.3 SpringSecurity Lombok MySQL8.0.23…
总述:
先根据三家总的说一下,大致可以划分为两类: **1.**前后端不分离 **2.**前后端分离
SpringSecurity概述
这一部分就是简述它的。
它是一款重量级安全框架,说起安全框架就立马想起两个关键词“认证”和“授权”。
一般来说,Web 应用的安全性包括**用户认证(Authentication)和用户授权(Authorization)**两个部分,这两点也是 Spring Security 重要核心功能。 英语单词务必给我记住
(1)用户认证指的是:就是系统认为用户是否能登录,账号密码匹配的问题
(2)用户授权指的是:就是系统判断用户是否有权限去做某些事情,比如vip权限可以干啥,普通用户却不行
例子:老婆与卧室
老婆在卧室里面睡觉,
情况一:这时响起了敲门声,通过猫眼看是你(老公),认证通过,授权老公身份,允许进入卧室,嘿嘿嘿;
情况二:通过猫眼看是隔壁老王(熟人),认证通过,没有权限跟老婆一起进卧室,老王黯然神伤;
情况三:陌生人,客厅门都进不来
同台竞争对手:Shiro(大致学过,估计忘记了,轻量级)
技术选型推荐:SSM + Shiro SpringBoot + SpringSecurity
-前后端不分离
下面代码块中标记有项目代码的,就可作为练手的,即写进项目自行测试的(记号为 @@txf)
–入门案例
新建一个SpringBoot项目
目录结构如图:
需要引入的依赖:@@txf
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--先把这个注释掉-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-security</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
</dependencies>
HelloController.java @@txf
@Controller
public class HelloController {
@RequestMapping("/hello")
public String HelloPage() {
return "hello";
}
}
hello.html @@txf(templates下面)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>hello</title>
</head>
<body>
这是hello界面
</body>
</html>
启动项目,浏览器运行 localhost:8080/hello
会直接跳转到templates下面的hello.html界面
然后将pom.xml里面的security依赖加上,刷新maven,重新运行项目,浏览器输入 localhost:8080/hello
出现如下界面:
我们发现地址栏自动发生了变化,界面也不是hello了,在控制台我们甚至可以看到这么一出
这就说明,SpringSecurity拦截到了我们的/hello请求,发现没有进行认证(登录),就给我们默认处理,跳转到了这个界面(默认的)。
此时,username输入user, password复制控制到输出的密码,就可以进行登录了。以上是Security默认生成了密码,为第一种方式;第二种方式:在配置文件中自己配置username和password。如下
application.properties或者application.yml @@txf
server.port=8080 #这是我自己配置的端口号,爱写不写,还是养成写的好习惯
#在配置文件中配置账号密码
spring.security.user.name=txf
spring.security.user.password=123456
配置完重新启动项目,发现控制台不会自动生成密码了,此时localhost:8080/hello跳转去/login时用我们在配置文件中的账号密码登录即可。登录完成就可以看到hello了。
你如果账号密码输入的不正确,肯定是通过不了的,自己动手实践去。
如果这样写账号密码的话,是固定的,实际场景肯定是从数据库中查到的!!!!!!!!
–加入数据库
引入依赖 @@txf
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.23</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3</version>
</dependency>
创建数据库test,创建表customer: @@txf(数据库建表)
DROP TABLE IF EXISTS `customer`;
CREATE TABLE `customer` (
`id` int NOT NULL AUTO_INCREMENT,
`realName` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '真实姓名',
`username` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '账号',
`password` varchar(20) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '密码',
`age` int NULL DEFAULT 20 COMMENT '年龄',
`adress` varchar(80) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '家庭住址',
`gender` int NULL DEFAULT 1 COMMENT '性别',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of customer
-- ----------------------------
INSERT INTO `customer` VALUES (1, '田小锋', 'txfnbnbnb', '123456', 20, '湖北省荆门市京山市', 1);
里面加入了一条数据,那就是我了。。。
配置文件中肯定是要修改的,加入下面这些 @@txf
#数据库
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.url=jdbc:mysql://localhost:3306/test?characterEncoding=utf-8&serverTimezone=GMT%2B8&useUnicode=true&allowMultiQueries=true
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
mybatis-plus.type-aliases-package=com.feng.securitydemo01.pojo
mybatis-plus.mapper-locations=classpath:/mapper/*.xml
实体类pojo包 @@txf
package com.feng.securitydemo01.pojo;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("customer")
public class Customer {
@TableId
private Integer id;
private String realname;
private String username;
private String password;
private Integer age;
private String adress;
private Integer gender;
}
Mapper接口 @@txf
package com.feng.securitydemo01.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.feng.securitydemo01.pojo.Customer;
import org.apache.ibatis.annotations.Mapper;
import org.springframework.stereotype.Repository;
@Mapper
@Repository
public interface CustomerMapper extends BaseMapper<Customer> {
}
–原理初探
页面提交方式必须为 post 请求
先放一张别人的图,SpringSecurity本质是一个过滤器链。
大致流程:
各个过滤器及其顺序:
上图只需要明白流程,不要记!大致是从顶部先一步一步调用走到底部,然后从底部返回值一步一步回到顶部
我们在它默认的登录页面输入的username和password,经过上图一系列过程,最终到UserDetailsService中。这个UserDetailsService是一个接口。
package com.feng.securitydemo01.service.Impl;
@Service //这个就不用多说了吧,交给Spring容器来管理,替换掉默认的
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//在这个方法里面,根据前端页面传过来的username,我们去数据库做一系列查询操作,
//返回值是一个UserDetails,
}
}
//==================下面是具体实现===================== @@txf
package com.feng.securitydemo01.service.Impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.feng.securitydemo01.mapper.CustomerMapper;
import com.feng.securitydemo01.pojo.Customer;
import com.feng.securitydemo01.pojo.LoginCustomer;
import org.springframework.beans.factory.annotation.Autowired;
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.Objects;
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private CustomerMapper customerMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Customer> wrapper = new QueryWrapper<>();
wrapper.eq("username",username);
Customer customer = customerMapper.selectOne(wrapper);
if (Objects.isNull(customer) ) { //空的,说明查不到
throw new UsernameNotFoundException("用户名不存在!");
}
//查到了,注意,这里现在我们不需要密码校验,直接封装为UserDetails返回就行了
//UserDetail我们自己实现
return new LoginCustomer(customer);
}
}
UserDetails
//点进源码发现这也是一个接口,是接口我们就可以去实现它,来完成自定义(虽然它已经有实现类了)
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
//===============================源码===========================================
//LoginCustomer.java 自定义实现UserDetails接口
//=====================具体实现=========== @@txf
package com.feng.securitydemo01.pojo;
import lombok.AllArgsConstructor;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
@Data
@AllArgsConstructor
public class LoginCustomer implements UserDetails {
private Customer customer; //存入我们根据username查到的customer
@Override
public Collection<? extends GrantedAuthority> getAuthorities() { //获取权限,先不看
return null;
}
@Override
public String getPassword() {
return customer.getPassword();
}
@Override
public String getUsername() {
return customer.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() { //能使用
return true;
}
}
–流程分析
根据上面的流程图文字解析:
-
根据传过来的username和password构造UsernamePasswordAuthenticationToken;(UsernamePasswordAuthenticationToken继承自AbstractAuthenticationToken,抽象类AbstractAuthenticationToken实现Authentication接口。)
-
调用方法ProviderManager.authenticate(参数类型是Authentication) ,所以可以传进去上面构造的User…Token。
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
..........................
Iterator var9 = this.getProviders().iterator();
while(var9.hasNext()) {
.......
//这里就是AbstractUserDetailsAuthenticationProvider的authenticate,
result = provider.authenticate(authentication);
.......
}
抽象类AbstractUserDetailsAuthenticationProvider的authenticate,
user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
- AbstractUserDetailsAuthenticationProvider的子类DaoAuthenticationProvider.retrieveUser()
protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
.................
//这里用上了我们的UserDetailsService!!!!!!!!!!!
//是不是就返回了UserDetails!!!
UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
...................
}
//看看这个方法是干什么的???????????
protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
this.logger.debug("Failed to authenticate since no credentials provided");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
} else {
String presentedPassword = authentication.getCredentials().toString();
//=========================
// this.passwordEncoder 密码校验,知道为什么不用你自己写了吧。自定义也行
//============================
if (!this.passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
this.logger.debug("Failed to authenticate since password does not match stored value");
throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
}
}
}
–加密
java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id “null”
还有一种是什么not like 什么什么的,
//密码加密
//假如数据库泄露了,密码如果是明文存储,那还搞个鬼子!!!!!!!!!所以,安全框架嘛,必然考虑得如此周到
/*
我们存进数据库里面的密码,那就肯定是要加密的了,即存进去的是密文 @@txf
*/
package com.feng.securitydemo01.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
//加密测试
@Test
public void test() {
String s = "123456";
String s1 = encoder.encode(s);
String s2 = encoder.encode(s);
System.out.println(s1 +" " + s2);
}
//输出结果,居然不一样!!!!这我们就不深究了。。。。反正都是123456
$2a$10$ipQhvDpdCZrpAc.JR7og/.l.3EpPovD2NlMcpUnYH4xv8inMemhI6 $2a$10$LDPVG9vvJvjBHsVfgLv0RO.2sq5LIscT01WBaybTf7bpQ.XHFOASa
//密码匹配测试
@Test
public void test() {
String s = "123456";
boolean matches = encoder
.matches(s,"$2a$10$ipQhvDpdCZrpAc.JR7og/.l.3EpPovD2NlMcpUnYH4xv8inMemhI6");
System.out.println(matches); //true
}
所以数据库中的123456要改咯,改成密文。将配置文件中自定义的username和password注掉,再来启动项目测试。这时候输入的就是数据库中的了。
数据中:
1 田小锋 txf $2a$10$ipQhvDpdCZrpAc.JR7og/.l.3EpPovD2NlMcpUnYH4xv8inMemhI6 20 湖北省荆门市京山市 1
启动项目:localhost:8080/hello 这时候输入自己的账号密码即可。
仔细看上面两个流程图,流程一定得明白,源码才勉强看得懂
–小项目
在上面的基础之上,改造页面,权限访问那些
授权基本流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
然后设置我们的资源所需要的权限即可
数据添加:(后面两个的密码是txf123)
用户拥有某一种角色,角色对应有哪些权限。上面这些表的意思。(走后门、查找、浏览)三种权限,admin所有权限都有,vip只有两种,user只有一种。四张表的名字分别是customer,cusrole,cusauth,cusroleauth。
有了权限,我们需要从数据库里面根据customer的id查询该用户所拥有的权限名称。
SELECT authname
FROM cusauth
WHERE authid in (
SELECT authid
FROM cusroleauth
WHERE roleid = (SELECT roleid
FROM customer
WHERE id = 1)
)
#这里可不可以请大佬帮我换一种方式查出来。。。本人着实有点愚钝
Mapper接口里面 @@txf
@Mapper
@Repository
public interface CustomerMapper extends BaseMapper<Customer> {
List<String> getAuthorizaById(Integer id);
}
Mapper.xml ( resourses下的mapper目录里面CustomerMapper.xml) @@txf
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.feng.securitydemo01.mapper.CustomerMapper">
<select id="getAuthorizaById" resultType="string" parameterType="int">
SELECT authname
FROM cusauth
WHERE authid in (
SELECT authid
FROM cusroleauth
WHERE roleid = (SELECT roleid
FROM customer
WHERE id = #{id})
)
</select>
</mapper>
修改
MyUserDetailsService和loginCustomer @@txf
//DetailsService
//查询权限封装进去
List<String> authoriza = customerMapper.getAuthorizaById(customer.getId());
//查到了,注意,这里现在我们不需要密码校验,直接封装为UserDetails返回就行了
//UserDetail我们自己实现
return new LoginCustomer(customer,authoriza);
//LoginUserDetailsService.java
private Customer customer;
private List<String> myAuthorities;
//存储SpringSecurity所需要的权限信息的集合
private List<GrantedAuthority> authorities;
public LoginCustomer(Customer customer, List<String> myAuthorities) {
this.customer = customer;
this.myAuthorities = myAuthorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
//myAuthorities
authorities = myAuthorities.stream().
map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
@EnableGlobalMethodSecurity(prePostEnabled = true)
//主启动类上添加此注解,先开启相关配置。
HelloController @@txf
package com.feng.securitydemo01.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
public class HelloController {
@RequestMapping({"/","/index"})
public String index() {
return "index";
}
@RequestMapping("/toLogin")
public String toLogin() {
return "login";
}
@GetMapping("/noauth")
public String accessDenyPage(){
return "noauth"; }
@RequestMapping("/houmen")
@PreAuthorize("hasAuthority('zouhoumen')") //看登录的用户是否有此权限。
public String houmen() {
return "admin/houmen";
}
@RequestMapping("/find")
@PreAuthorize("hasAuthority('find')")
public String find() {
return "vip/find";
}
@RequestMapping("/hello")
@PreAuthorize("hasAuthority('look')")
public String HelloPage() {
return "hello";
}
}
此外,我们希望对首页,不需要登录就能访问怎么做呢? @@txf
@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
//================这中间配置即可
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //关闭csrf
.formLogin()
// .usernameParameter("自己根据情况写") 自定义表单的username,password
// .passwordParameter("自己情况写")
//还有rememberme、logout功能,极其简单,这里就不说了
.loginProcessingUrl("/login") //登录的url
.successForwardUrl("/index") //登录成功后走/index
.and()
.authorizeRequests().antMatchers("/","/index","/toLogin").permitAll() //"/","/index"放行
.anyRequest().authenticated(); //所有请求都需要认证
//配置403处理器,修改response中的内容,
//http.exceptionHandling().accessDeniedHandler(myAccessFail);
//自定义403页面,前后端不分离
http.exceptionHandling().accessDeniedPage("/noauth");
}
//==================
}
403处理器
package com.feng.securitydemo01.config;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
@Component
public class MyAccessFail implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
//设置响应状态码
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
//设置响应数据格式
response.setContentType("application/json;charset=utf-8");
//输入响应内容
PrintWriter writer = response.getWriter();
String json="{\"status\":\"403\",\"msg\":\"无权访问\"}";
writer.write(json);
writer.flush();
}
}
package com.feng.securitydemo01.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
MyAccessFail myAccessFail;
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable() //关闭csrf
.formLogin()
// .usernameParameter("自己根据情况写") 自定义表单的username,password
// .passwordParameter("自己情况写")
//还有rememberme、logout功能,极其简单,这里就不说了
.loginProcessingUrl("/login") //登录的url
.successForwardUrl("/index") //登录成功后走/index
.and()
.authorizeRequests().antMatchers("/","/index","/toLogin").permitAll() //"/","/index"放行
.anyRequest().authenticated(); //所有请求都需要认证
//配置403处理器,修改response中的内容,前后端分离,是根据响应体来具体判断的。
//http.exceptionHandling().accessDeniedHandler(myAccessFail);
//自定义403页面,前后端不分离
http.exceptionHandling().accessDeniedPage("/noauth");
}
}
源码
https://download.csdn.net/download/okok__TXF/80962384
-前后端分离
如果前文误人子弟太多了,就不写了,这篇也删了。。。根据情况来写吧,