文章目录
- 一、快速入门
- 二 、认证授权
- 三 、整合项目
一、快速入门
概述
Spring Security 是 Spring 家族中的一个安全管理框架,应用程序的两个主要区域是“认证”和“授权”(或者访问控制)
认证:
系统提供的用于识别用户身份的功能,通常提供用户名和密码进行登录其实就是在进行认证,认证的目的是让系统知道你是谁。
授权:
用户认证成功后,需要为用户授权,其实就是指定当前用户可以操作哪些功能。
权限数据模型
前面已经分析了认证和授权的概念,要实现最终的权限控制,需要有一套表结构支撑:
用户表t_user、权限表t_permission、角色表t_role、菜单表t_menu、用户角色关系表t_user_role、角色权限关系表t_role_permission、角色菜单关系表t_role_menu。
上述的7张表就构成了RBAC权限模型:
- 导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--SpringSecurity起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 编写controller
package com.execise.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class Controller01 {
@RequestMapping("/show")
public String show(){
System.out.println("执行了show方法!~~!");
return "show ... success...";
}
}
- 访问controller
- 会出现登录页面,表明springsecurity已经开始工作
- 输入默认的用户名: user , 密码通过控制台可以找到springsecurity产生的密码
- 细节处理:
自定义用户名和密码
在application.yml里面可以配置自定义的用户名和密码
修改日志级别
默认打印的springsecurity的日志级别是 info级别,如果想观察到更详细的日志信息,可以在application.yml 里面修改日志的打印级别 。 如果info看不到日志,可以尝试再设置低一些级别:
比如: debug 或者 trace 级别
log.info()、log.error()
日志级别:fatal>error>warn>info>debug>trace
logging:
level:
org:
springframework:
security: info
- 自动配置原理
springboot自动配置
默认情况下,只要在springboot项目里面添加了springsecurity依赖,那么springsecurity即会自动工作。这个自动生效的配置是由springboot来完成的。
- 在启动类身上的注解
@SpringBootApplication
即是一切自动配置的开始 - 进入该注解内部有一个注解 :
@EnableAutoConfiguration
, 表示启动自动配置 - 在
@EnableAutoConfiguration
注解里面,导入了AutoConfigurationImportSelector
自动配置导入选择器 - 在这个自动导入选择器类
AutoConfigurationImportSelector
中的getCandidateConfigurations
可以看到自动导入的类位于META-INF/spring.factories
文件中。 - 该文件位于 springboot的autoconfigure包中
- 在spring.factories文件中搜索security关键字,找到执行 springsecurity 自动配置的类:SecurityAutoConfiguration
- 在SecurityAutoConfiguration里面发现它在上面使用 @Import导入 SpringBootWebSecurityConfiguration 类。
- SpringBootWebSecurityConfiguration 会对所有的请求进行拦截,自此springsecurity的自动配置解析完毕:
springsecurity原理分析
springsecurity的核心即是: 过滤器Filter , 翻开springsecurity的官方文档,找到如下说明:
- 这个对象的产生是位于:WebSecurityConfiguration中的springSecurityFilterChain方法中
- 通过debug发现,总共有15个过滤器需要配置,这些过滤器各司其职,每个过滤器负责的功能都不一样!
- 参照官方文档的说明,可以看到有对springsecurity过滤器的描述、以及流程解释:
二 、认证授权
在DefaultWebSecurityCondition 里面存在注解
@ConditionalOnMissingBean({WebSecurityConfigurerAdapter.class,… }) 表明如果在JVM中缺失
WebSecurityConfigurerAdapter 则会启用默认的springsecurity的认证和授权流程。 所以如果我们希望自己执行认
证和授权,那么则需要编写一个类,继承 WebSecurityConfigurerAdapter 即可。
- 内存方式
一般在开发当中,我们都会选择自己来认证授权!
定义配置类
//为了让spring发现我们写的配置类,需要加上 @Configuration
@Configuration
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
}
重写方法
认证方法
/*
认证:
什么样的账号和密码, 是什么样的角色
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//内存中的认证:
auth.inMemoryAuthentication()
.withUser("zhangsan")
.password("{noop}123456")
.roles("ZS")
.and()
.withUser("admin")
.password("{noop}666")
.roles("ADMIN");
}
授权方法
/**
* 授权:
* 1. 什么样的请求地址允许直接放行,
* 2. 什么样的请求需要有角色权限,'
* 3. 其他的请求全部要求认证通过之后才能访问。
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 直接访问,不需要登录
.antMatchers("/login.html" , "/**/*.css"
, "/**/*.html", "/**/*.js").permitAll()
//需要角色是AD
.antMatchers("/show","/show01","/show02").hasRole("ADMIN")
.anyRequest().authenticated()
.and() //and 用来拼接配置
.formLogin(); // 表示使用默认springsecurity的登录页面。
}
异常处理:
当我们已经登录,但是访问并不具有访问权限的资源时,那么会出现403 的异常:
解决办法: 在授权方法里面添加关于异常的处理办法:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 直接访问,不需要登录
.antMatchers("/login.html" , "/**/*.css" , "/*/*.html", "/**/*.js").permitAll()
//需要角色是AD
.antMatchers("/show","/show01","/show02").hasRole("admin")
.anyRequest().authenticated()
.and() //and 用来拼接配置
.formLogin(); // 表示使用默认springsecurity的登录页面。
//异常处理
http.exceptionHandling()
.accessDeniedHandler(new AccessDeniedExceptionHandler());
}
异常处理类
public class AccessDeniedExceptionHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("text/html;charset=utf-8");
response.getWriter().write("权限不足,禁止访问!");
}
}
自定义登录页面
默认springsecurity的登录操作,会被过滤器: UsernamePasswordAuthenticationFilter 拦截,它内部有几个判定: 请求方式是 post, 请求地址是 /login , 用户名参数是: username , 密码参数名是: password
请求方式不能修改之外,其他的都可以修改
定义登录页面
<form action="/login" method="post">
用户名: <input type="text" name="username"/><br/>
密 码: <input type="text" name="password"/><br/>
<input type="submit" value="登录"/>
</form>
配置
/**
* 授权:
* 1. 什么样的请求地址允许直接放行,
* 2. 什么样的请求需要有角色权限,'
* 3. 其他的请求全部要求认证通过之后才能访问。
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") //登录页面
.usernameParameter("username") //用户名参数名称
.passwordParameter("password") //密码参数名称
.loginProcessingUrl("/login") //登录的请求地址
.defaultSuccessUrl("/index.html",true) //登录成功之后的页面地址
.failureForwardUrl("/login.html") //登录失败之后的页面地址
.and().csrf().disable();
}
csrf
CSRF(Cross-site request forgery),中文名称:跨站请求伪造,缩写为:CSRF/XSRF。
一般来说,攻击者通过伪造用户的浏览器的请求,向访问一个用户自己曾经认证访问过的网站发送出去,使目标网站接收并误以为是用户的真实操作而去执行命令。常用于盗取账号、转账、发送虚假消息等。
退出登录
/**
* 授权:
* 1. 什么样的请求地址允许直接放行,
* 2. 什么样的请求需要有角色权限,'
* 3. 其他的请求全部要求认证通过之后才能访问。
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/login.html") //登录页面
.usernameParameter("username") //用户名参数名称
.passwordParameter("password") //密码参数名称
.loginProcessingUrl("/login") //登录的请求地址
.defaultSuccessUrl("/index.html") //登录成功之后的页面地址
.failureForwardUrl("/login.html") //登录失败之后的页面地址\
.and().csrf().disable() //禁用csrf
.logout() //退出登录配置
.logoutUrl("/logout") //退出请求
.logoutSuccessUrl("/login.html"); //退出成功到达页面
}
密码加密
在springsecurity里面,支持的加密方式有很多, 具体可以在PasswordEncoderFactories 里面查看。官方推荐使用 bcrypt 加密方式。
MD5:加密方式 不可逆 但是每次加密的结果都是一样的
针对它的缺陷,解决方式:
- 方式一:加密多次 至少3次以上
- 方式二:Md5(password+salt) salt:随机字符串 UUID
bcrypt:将salt随机并混入最终加密后的密码,验证时也无需单独提供之前的salt,从而无需单独处理salt问题
spring security中的BCryptPasswordEncoder方法采用SHA-256 +随机盐+密钥 对密码进行加密。SHA系列是Hash算法,不是加密算法,使用加密算法意味着可以解密(这个与编码/解码一样),但是采用Hash处理,其过程是不可逆的。
(1)加密(encode):注册用户时,使用SHA-256+随机盐+密钥把用户输入的密码进行hash处理,得到密码的hash值,然后将其存入数据库中。
(2)密码匹配(matches):用户登录时,密码匹配阶段并没有进行密码解密(因为密码经过Hash处理,是不可逆的),而是使用相同的算法把用户输入的密码进行hash处理,得到密码的hash值,然后将其与从数据库中查询到的密码hash值进行比较。如果两者相同,说明用户输入的密码正确。
这正是为什么处理密码时要用hash算法,而不用加密算法。因为这样处理即使数据库泄漏,黑客也很难破解密码。
加密结果解释:
$2a$10$/bTVvqqlH9UiE0ZJZ7N2Me3RIgUCdgMheyTgV0B4cMCSokPa.6oCa
加密后字符串的长度为固定的60位。其中:
$是分割符,无意义;
2a是bcrypt加密版本号;
10是循环10次加盐加密;
而后的前22位是salt值;
再然后的字符串就是密码的密文了。
具体应用
要想使用bcrypt密码加密其实很简单,只需要在配置类中定义一个方法,返回BcryptPasswordEncoder 对象即可,并且不要忘记了对认证的用户密码进行加密处理。
用户在输入密码登录时,SpringSecurity就会自动对用户输入的密码使用bcrypt进行加密 然后和内存|数据库中存储的用户密码进行比对 注意:内存|数据库中存储的用户密码也要使用bcrypt进行加密
密码加密使用步骤:
- 对用户输入的密码进行加密
- 对内存|数据库中存储的密码进行加密
注意:两边的加密方式要一致
添加方法
@Bean
public BCryptPasswordEncoder bp (){
return new BCryptPasswordEncoder();
}
认证处理
/*
认证:
什么样的账号和密码, 是什么样的角色
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//内存中的认证:
auth.inMemoryAuthentication()
.withUser("zhangsan")
//.password("{noop}123456") // 明文密码
// bcrpt加密处理 , 使用这种方式,就不需要在上面的bp() 上面打上注解 @Bean了
//.password("{bcrypt}"+bp().encode("123456"))
.password(bp().encode("123456")) // bcrpt加密处理
.roles("USER")
.and()
.withUser("admin")
.password(bp().encode("666"))
.roles("ADMIN");
}
- 数据库方式
# 创建用户表
create table t_user(
id bigint primary key auto_increment ,
username varchar(25) ,
password varchar(65) ,
name varchar(25));
# 创建角色表
create table t_role(
id int primary key auto_increment,
name varchar(25) ,
keyword varchar(25)
);
# 创建用户角色表
create table t_user_role(uid bigint , rid int );
# 添加外键
alter table t_user_role add constraint FK_user_ur foreign key (uid) references t_user(id);
alter table t_user_role add constraint FK_role_ur foreign key (rid) references t_role(id);
# 添加数据 密码是 123456 加密后的效果
insert into t_user values ( null , 'zhangsan' , '$2a$10$ezVuPO6NaUsQZ66p7y0QSeWfO6s.Qoz01vbTcI2vlcuLXy8Wk.DOy' , '张三');
insert into t_user values ( null , 'lisi' , '$2a$10$ezVuPO6NaUsQZ66p7y0QSeWfO6s.Qoz01vbTcI2vlcuLXy8Wk.DOy' , '李四');
insert into t_role values ( null , '管理员' , 'ROLE_ADMIN' );
insert into t_role values ( null , '普通员工' , 'ROLE_USER' );
insert into t_user_role value( 1 , 1 );
insert into t_user_role value( 2 , 2 );
创建项目,添加mybatisplus、mysql驱动、lombok依赖
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.2</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
application.yml中配置数据源
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/day44?useSSL=false&serverTimezone=UTC
username: root
password: root
准备实体类
User
package com.execise.bean;
import lombok.Data;
import java.util.List;
@Data
public class User {
private long id ;
private String username;
private String password;
private String name;
//用于记录用户的角色信息,一个用户可以有多个角色身份。
private List<Role> roleList;
}
Role
package com.execise.bean;
import lombok.Data;
@Data
public class Role {
private long id ;
private String name;
private String keyword;
}
准备dao
UserDao
package com.execise.dao;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.execise.bean.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface UserDao extends BaseMapper<User> {
/**
* 根据用户名查询用户
* @param username
* @return
*/
User findByUsernameUser(String username);
}
UserDao.xml
位于: static/mapper/UserDao.xml
<resultMap id="userMap" type="User">
<id property="id" column="id"/>
<result property="username" column="username" />
<result property="password" column="password" />
<result property="name" column="name" />
<!--collection:配置一对多 将查询出来的每条角色信息封装到一个Role对象中 最终存入到roleList集合中-->
<collection property="roleList" ofType="Role" columnPrefix="r_">
<result property="id" column="id"/>
<result property="name" column="name"/>
<result property="keyword" column="keyword"/>
</collection>
</resultMap>
<select id="findByUsernameUser" resultMap="userMap">
select u.*,r.id r_id,r.name r_name,r.keyword r_keyword from t_user AS u
inner join t_user_role AS ur on u.id=ur.uid
inner join t_role AS r on ur.rid = r.id
where username=#{username}
</select>
application.yml
mybatis-plus:
type-aliases-package: com.execise.bean
mapper-locations: classpath:mapper/*.xml
准备配置类
package com.execise.service.impl;
import com.execise.bean.Role;
import com.execise.bean.User;
import com.execise.dao.UserDao;
import org.springframework.beans.factory.annotation.Autowired;
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.List;
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//1. 根据用户名查询用户
User user = userDao.findByUsernameUser(username);
System.out.println("user = " + user);
//2. 构建返回
List<GrantedAuthority> list = new ArrayList<>();
List<Role> roleList = user.getRoleList();
for (Role role : roleList) {
list.add(new SimpleGrantedAuthority(role.getKeyword()));
}
//返回用户详情信息
return new org.springframework.security.core.userdetails.User(
user.getUsername() ,
user.getPassword() ,
list);
}
}
修改认证方法
@Configuration
public class SecurityConfigure extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailServiceImpl us;
@Bean
public BCryptPasswordEncoder bp (){
return new BCryptPasswordEncoder();
}
/*
认证:
什么样的账号和密码, 是什么样的角色
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(us);
}
//...授权方法
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**/*.html","/**/*.css","/**/*.js").permitAll()
.antMatchers("/show").hasRole("ADMIN")
.antMatchers("/show02").hasRole("USER")
.anyRequest().authenticated()
.and().formLogin()
.loginPage("/login.html")
.loginProcessingUrl("/login").defaultSuccessUrl("/index.html",true).failureForwardUrl("/login.html")
.and().csrf().disable()
.logout().logoutUrl("/logout").logoutSuccessUrl("/login.html")
//403异常处理
.and().exceptionHandling().accessDeniedHandler(new AccessDeniedHandler() {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("text/html;charset=UTF-8");
response.getWriter().print("权限不足 禁止访问");
}
});
}
}
注解动态授权
springsecurity允许程序员在Controller方法上使用注解来动态授权,即配置角色|权限到方法上。表明需要具备什么样的角色身份或者是权限,才允许访问该方法!
- 在方法上使用 @PreAuthorize 进行调用 权限拦截
//表明调用方法需具有该角色身份。
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/show66")
public String show66(){
System.out.println("执行了show66方法!~~!");
return "show ... success...";
}
- 要想让注解生效,需要在启动类上面设置注解
//启用全局方法注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
@SpringBootApplication
public class Demo1HelloworldApplication {
public static void main(String[] args) {
SpringApplication.run(Demo1HelloworldApplication.class, args);
}
}
三 、整合项目
- 添加依赖
<!--添加springsecurity-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
- 创建表
角色表
create table role
(
id bigint auto_increment primary key,
name varchar(100) null,
keyword varchar(25) null
);
员工角色表
create table if not exists employee_role
(
eid bigint null,
rid bigint null,
constraint user_role_role_id_fk
foreign key (rid) references role (id),
constraint user_role_user_id_fk
foreign key (eid) references employee (id)
);
- 编写实体类
Role
package com.execise.bean;
import lombok.Data;
@Data
public class Role {
private long id ;
private String name;
private String keyword;
}
Employee
让Employee实现UserDetails 这样在认证方法返回即可只返回Employee对象即可
因为它里面包含了权限内容
@Data
public class Employee implements Serializable , UserDetails {
private static final long serialVersionUID = 1L;
@TableField(exist = false)
private List<Role> roleList = new ArrayList<>();
//原有属性省略...
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list = new ArrayList<>();
roleList.forEach(r->{
list.add(new SimpleGrantedAuthority(r.getKeyword()));
});
return list;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
- 编写Dao
public interface EmployeeDao extends BaseMapper<Employee> {
/**
* 根据用户名来查询员工信息及角色身份列表信息
* @param username
* @return
*/
Employee findByUsername(String username);
}
- 编写映射文件
EmployeeMapper.xml
<?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.execise.dao.EmployeeDao">
<resultMap id="empMap" type="Employee">
<id property="id" column="id"/>
<result property="name" column="name"/>
<result property="username" column="username"/>
<result property="password" column="password"/>
<collection property="roleList" ofType="Role" columnPrefix="r_">
<result property="id" column="id"/>
<result property="name" column="name"/>
<result property="keyword" column="keyword"/>
</collection>
</resultMap>
<select id="selectByUsername" resultMap="empMap">
select e.*,r.id r_id,r.name r_name,r.keyword r_keyword from employee as e
inner join employee_role er on e.id = er.eid
inner join role r on er.rid = r.id
where e.username=#{username}
</select>
</mapper>
application.yml
---
mybatis-plus:
mapper-locations: classpath:/mapper/*.xml
type-aliases-package: com.execise.bean
- 编写认证处理类
package com.execise.security;
import com.execise.bean.Employee;
import com.execise.bean.Role;
import com.execise.dao.EmployeeDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
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.List;
@Service
public class EmployeeDetailsServiceImpl implements UserDetailsService {
@Autowired
private EmployeeDao dao;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
//根据用户名来查询用户信息
return dao.findByUsername(s);
}
}
7 . 编写过滤器类
package com.execise.security;
import com.alibaba.fastjson.JSON;
import com.execise.bean.Employee;
import com.execise.config.BaseContext;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* 自己定义的过滤器,用来顶替springsecurity的账号密码校验的过滤器
*/
public class LoginFilter02 extends UsernamePasswordAuthenticationFilter {
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
try {
//如果不是post请求,那么直接抛出异常,因为一般登录操作,都是走post请求。
if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
} else {
//获取登录 ajax请求携带的json格式用户名和密码封装到Employee对象中
Employee e = JSON.parseObject(request.getInputStream() , Employee.class ) ;
System.out.println("LoginFilter02:: 页面传递上来的对象:" + e);
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(e.getUsername(), e.getPassword());
this.setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
} catch (IOException e) {
e.printStackTrace();
}
return super.attemptAuthentication(request,response);
}
}
- 编写认证授权配置类
package com.execise.security;
import com.alibaba.fastjson.JSON;
import com.execise.bean.Employee;
import com.execise.common.R;
import com.execise.config.BaseContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.util.MimeType;
import org.springframework.util.MimeTypeUtils;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
public class SpringSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private BaseContext baseContext;
@Autowired
private EmployeeDetailsServiceImpl es;
@Bean
public BCryptPasswordEncoder bcr(){
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(es);
}
@Bean
public LoginFilter02 loginFilter02 () throws Exception {
//1.构建登录过滤器对象
LoginFilter02 filter = new LoginFilter02();
//2.设置登录过滤器拦截地址
filter.setFilterProcessesUrl("/employee/login");
//3.设置认证管理员 将自定义的登录过滤器管理器加入到SpringSecurity认证管理员管理
filter.setAuthenticationManager(authenticationManagerBean());
//4.设置登录成功处理
filter.setAuthenticationSuccessHandler((req,resp,auth)->{
String json = JSON.toJSONString(R.success("登录成功"));
resp.setContentType("application/json;charset=utf-8");
resp.getWriter().write(json);
});
//5.设置登录失败处理
filter.setAuthenticationFailureHandler((req, resp ,ex)->{
resp.setContentType("application/json;charset=utf-8");
String json = JSON.toJSONString(R.error("登录失败"));
resp.getWriter().write(json);
});
return filter;
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
.loginPage("/backend/page/login/login.html").permitAll()
.and().logout().logoutUrl("/logout")
.logoutSuccessHandler((req, resp , auth) ->{
String json = JSON.toJSONString(R.success("退出登录成功"));
resp.getWriter().write(json);
})
.and().authorizeRequests()
.antMatchers("/**/*.js" , "/**/*.css" , "/**/styles/**","/sms/code" , "/**/*.ico" , "/**/images/**").permitAll()
.anyRequest().authenticated()
.and().headers().frameOptions().sameOrigin() //加入同源策略,这样即可允许页面上加载iframe子页面
.and().csrf().disable();
http.addFilterAt(loginFilter02() , UsernamePasswordAuthenticationFilter.class);
}
}
- 修改公共字段填充
package com.execise.config;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.execise.bean.Employee;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpSession;
import java.time.LocalDateTime;
//把这个类交给spring管理
@Component
public class CommonFieldHandler implements MetaObjectHandler {
@Autowired
private BaseContext baseContext;
@Override
public void insertFill(MetaObject metaObject) {
System.out.println("添加操作::: 来调用insertFill给公共字段赋值了!~~!");
metaObject.setValue("createTime", LocalDateTime.now());
metaObject.setValue("updateTime", LocalDateTime.now());
//从springsecurity作用域对象中获取当前管理的认证用户对象【相当于session中存储】
Employee e = (Employee) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
metaObject.setValue("createUser" , e.getId());
metaObject.setValue("updateUser" , e.getId());
}
//当mybatisplus 执行更新操作的时候,会通过这个方法给公共字段赋值。
@Override
public void updateFill(MetaObject metaObject) {
metaObject.setValue("updateTime", LocalDateTime.now());
//从springsecurity中获取数据
Employee e = (Employee) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
metaObject.setValue("updateUser" ,e.getId());
}
}
- 使用动态鉴权
在category的添加分类方法上,使用注解标记需要什么样的权限才能访问该方法。
/**
* 添加分类
* @param category
* @return
*/
@PreAuthorize("hasRole('ADMIN')")
@PostMapping
public R<String> add(@RequestBody Category category){
//1. 调用service
int row = cs.add(category);
//2. 返回
if(row > 0 ){
return R.success("添加分类成功!");
}else{
return R.error("添加分类失败!");
}
}
启动类添加开启全局注解开关
package com.execise;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
@MapperScan("com.execise.dao") //扫描dao接口。
@SpringBootApplication
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class ReggieApplication {
public static void main(String[] args) {
SpringApplication.run(ReggieApplication.class , args);
}
}