文章目录
1、设置登录系统的账号、密码
- 第一种方式:通过配置文件
spring.security.user.name=zhangsan
spring.security.user.password=zhangsan
- 第二种方式:通过配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String password = bCryptPasswordEncoder.encode("123");
auth.inMemoryAuthentication().withUser("tom").password(password).roles("admin");
}
@Bean
PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
- 第三种方式:自定义编写实现类【推荐】
第一步:创建配置类,设置使用哪个userDetailsService实现类
@Configuration
public class SecurityConfigTest extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(userDetailsService).passwordEncoder(password());
}
@Bean
PasswordEncoder password(){
return new BCryptPasswordEncoder();
}
}
第二步:编写实现类,返回user对象,User对象有用户名密码和操作权限
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
return new User("marry",new BCryptPasswordEncoder().encode("123"),auths);
}
}
2、实现数据库认证来完成用户登录
完成自定义登录
1. 准备sql
create table users(
id bigint primary key auto_increment,
username varchar(20) unique not null,
password varchar(100)
);
-- 密码 atguigu
insert into users values(1,'lucy','123');
-- 密码 atguigu
insert into users values(2,'jack','123');
create table role(
id bigint primary key auto_increment,
name varchar(20)
);
insert into role values(1,'管理员');
insert into role values(2,'普通用户');
create table role_user(
uid bigint,
rid bigint
);
insert into role_user values(1,1);
insert into role_user values(2,2);
create table menu(
id bigint primary key auto_increment,
name varchar(20),
url varchar(100),
parentid bigint,
permission varchar(20)
);
insert into menu values(1,'系统管理','',0,'menu:system');
insert into menu values(2,'用户管理','',0,'menu:user');
create table role_menu(
mid bigint,
rid bigint
);
insert into role_menu values(1,1);
insert into role_menu values(2,1);
insert into role_menu values(2,2);
2. 添加依赖
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.0.5</version>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--lombok 用来简化实体类-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
3. 编写实体类
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Users {
private Integer id;
private String username;
private String password;
}
4. 整合 MybatisPlus 制作 mapper
@Repository
public interface UsersMapper extends BaseMapper<Users> {
}
#mysql 数据库连接
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=123456
在启动类上添加包扫描
@MapperScan("com.kuang.myspringsecurity.mapper")
5. 编写登录实现类
@Service("userDetailsService")
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//调用usersMapper方法,根据用户名查询数据库
QueryWrapper<Users> wrapper = new QueryWrapper<>();
// where username=?
wrapper.eq("username",username);
Users users = usersMapper.selectOne(wrapper);
//判断
if (users == null){
throw new UsernameNotFoundException("用户不存在");
}
List<GrantedAuthority> auths = AuthorityUtils.commaSeparatedStringToAuthorityList("role");
//从数据库查询返回users对象,得到用户名和密码
return new User(users.getUsername(),new BCryptPasswordEncoder().encode(users.getPassword()),auths);
}
}
6. 测试访问
localhost:8111/test/hello
3、自定义设置登录页面,不需要认证访问
1. 在配置类实现相关的配置
@Override
protected void configure(HttpSecurity http) throws Exception{
http.formLogin() //自定义自己编写的登录页面
.loginPage("/login.html") //登录页面设置
.loginProcessingUrl("/user/login") //登录访问请求
.defaultSuccessUrl("/test/index").permitAll() //登录成功之后,跳转的路径
.and().authorizeRequests()
.antMatchers("/","/test/hello","/user/login").permitAll() //设置哪些路径可以直接访问,不需要认证
.anyRequest() //其他请求
.authenticated() //需要认证
.and().csrf().disable(); //关闭csrf防护
}
2. 创建相关页面,controller
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="/user/login" method="post">
用户名:<input type="text" name="username">
<br>
密码:<input type="text" name="password">
<br>
<input type="submit" value="登录">
</form>
</body>
</html>
注意:页面提交方式必须为 post 请求,所以上面的页面不能使用,用户名,密码必须为username,password
原因:在执行登录的时候会走一个过滤器 UsernamePasswordAuthenticationFilter
如果修改配置可以调用 usernameParameter()和 passwordParameter()方法。
4、基于角色或权限进行访问控制
1. hasAuthority 方法
如果当前的主体具有指定的权限,则返回 true,否则返回 false。只允许添加一个权限
。
- 修改配置类:设置当前访问地址有哪些权限
- 修改实现类:在UserDetailsService,把返回User对象设置权限
这里添加的权限必须与配置类中设置的一致。
如果在访问的时候出现 403,表示没有访问权限。
2. hasAnyAuthority 方法
如果当前的主体有任何提供的角色(给定的作为一个逗号分隔的字符串列表)的话,返回true。允许添加多个权限
。
- 修改配置类:设置当前访问地址有哪些权限
- 修改实现类:在UserDetailsService,把返回User对象设置权限
3. hasRole 方法
如果用户具备给定角色就允许访问,否则出现 403。
如果当前主体具有指定的角色,则返回 true。
底层源码:
-
修改配置文件
注意:配置文件中不需要添加 ”ROLE_“,因为上述的底层代码会自动添加与之进行匹配。
-
给用户添加角色
4. hasAnyRole方法
表示用户具备任何一个条件都可以访问。
- 修改配置文件
- 修改实现类:给用户添加角色
5、基于数据库实现权限认证
1. 添加实体类
@Data
public class Role {
private Long id;
private String name;
}
@Data
public class Menu {
private Long id;
private String name;
private String url;
private Long parentId;
private String permission; //权限
}
2. 编写UsersMapper接口
/**
* 根据用户 Id 查询用户角色
* @param userId
* @return
*/
List<Role> selectRoleByUserId(Integer userId);
/**
* 根据用户 Id 查询菜单
* @param userId
* @return
*/
List<Menu> selectMenuByUserId(Integer userId);
3. 编写UsersMapper.xml文件
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"mybatis-3-mapper.dtd">
<mapper namespace="com.kuang.myspringsecurity.mapper.UsersMapper">
<!--根据用户 Id 查询角色信息-->
<select id="selectRoleByUserId" resultType="com.kuang.myspringsecurity.entity.Role">
SELECT r.id,r.name FROM role r INNER JOIN role_user ru ON
ru.rid=r.id where ru.uid=#{0}
</select>
<!--根据用户 Id 查询权限信息-->
<select id="selectMenuByUserId" resultType="com.kuang.myspringsecurity.entity.Menu">
SELECT m.id,m.name,m.url,m.parentid,m.permission FROM menu m
INNER JOIN role_menu rm ON m.id=rm.mid
INNER JOIN role r ON r.id=rm.rid
INNER JOIN role_user ru ON r.id=ru.rid
WHERE ru.uid=#{0}
</select>
</mapper>
4. 编写MyUserDetailsService实现类
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//调用usersMapper方法,根据用户名查询数据库
QueryWrapper<Users> wrapper = new QueryWrapper<>();
// where username=?
wrapper.eq("username",username);
Users users = usersMapper.selectOne(wrapper);
//判断
if (users == null){
throw new UsernameNotFoundException("用户不存在");
}
//获取到用户角色,菜单列表
List<Role> roleList = usersMapper.selectRoleByUserId(users.getId());
List<Menu> menuList = usersMapper.selectMenuByUserId(users.getId());
//声明一个集合
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
//处理角色
for (Role role : roleList) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority("ROLE_"+role.getName());
grantedAuthorityList.add(simpleGrantedAuthority);
}
//处理权限
for (Menu menu : menuList) {
grantedAuthorityList.add(new SimpleGrantedAuthority(menu.getPermission()));
}
//从数据库查询返回users对象,得到用户名和密码
return new User(users.getUsername(),new BCryptPasswordEncoder().encode(users.getPassword()),grantedAuthorityList);
}
5. 在配置文件中添加映射
mybatis-plus.mapper-locations=classpath:mapper/*.xml
6. 修改访问配置类
7. 使用管理员与非管理员进行测试
管理员:lucy,非管理员:jack
如果非管理员测试,会提示 403 没有权限
6、自定义304没有权限访问页面
1. 修改访问配置类
//自定义403请求
http.exceptionHandling().accessDeniedPage("/unauth.html");
2. 编写unauth.html页面
<body>
<h1>对不起,您没有访问权限!</h1>
</body>
3. 测试
登录之后使用jack用户进行测试
7、注解使用
1. @Secured
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀 “ROLE_
“。
使用注解先要开启注解功能,在启动类(配置类)中
@EnableGlobalMethodSecurity(securedEnabled=true)
在控制器方法上添加注解
@GetMapping("update")
@Secured({
"ROLE_管理员","ROLE_normal"})
public String update(){
return "hello update";
}
在MyUserDetailsService类中设置用户角色
我们这里是从数据库中查询的,所以就不用设置了。
登录之后直接访问使用lucy进行测试:localhost:8111/test/update
2. @PreAuthorize
首先在主启动类上开启注解:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize:注解适合进入方法前的权限验证, @PreAuthorize 可以将登录用户的 roles/permissions 参数传到方法中。
@GetMapping("update")
//@Secured({"ROLE_管理员","ROLE_normal"})
@PreAuthorize("hasAnyAuthority('menu:system')")
public String update(){
return "hello update";
}
3. @PostAuthorize
首先在主启动类上开启注解:
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PostAuthorize 注解使用并不多,在方法执行后再进行权限验证,适合验证带有返回值的权限。
@GetMapping("update")
//@Secured({"ROLE_管理员","ROLE_normal"})
//@PreAuthorize("hasAnyAuthority('menu:system')")
@PostAuthorize("hasAnyAuthority('menu:system')")
public String update(){
System.out.println("update....");
return "hello update";
}
4. @PostFilter
@PostFilter :权限验证之后对数据进行过滤,留下用户名是 admin1 的数据
表达式中的 filterObject 引用的是方法返回值 List 中的某一个元素
@RequestMapping("getAll")
@PreAuthorize("hasRole('ROLE_管理员')")
@PostFilter("filterObject.username == 'admin1'")
@ResponseBody
public List<Users> getAllUser(){
ArrayList<Users> list = new ArrayList<>();
list.add(new Users(11,"admin1","6666"));
list.add(new Users(22,"admin2","888"));
return list;
}
5. @PreFilter
@PreFilter: 进入控制器之前对数据进行过滤
@RequestMapping("getTestPreFilter")
@PreAuthorize("hasRole('ROLE_管理员')")
@PreFilter(value = "filterObject.id%2==0")
@ResponseBody
public List<Users> getTestPreFilter(@RequestBody List<Users> list){
list.forEach(t-> {
System.out.println(t.getId()+"\t"+t.getUsername());
});
return list;
}
6. 权限表达式
https://docs.spring.io/spring-security/site/docs/5.3.4.RELEASE/reference/html5/#el-access
8、基于数据库的记住我
1. 创建表
CREATE TABLE `persistent_logins` (
`username` varchar(64) NOT NULL,
`series` varchar(64) NOT NULL,
`token` varchar(64) NOT NULL,
`last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE
CURRENT_TIMESTAMP,
PRIMARY KEY (`series`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
2. 修改安全配置类
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
// 赋值数据源
jdbcTokenRepository.setDataSource(dataSource);
// 自动创建表,第一次执行会创建,以后要执行就要删除掉!
//jdbcTokenRepository.setCreateTableOnStartup(true);
return jdbcTokenRepository;
}
//开启记住我功能
.and().rememberMe()
.tokenValiditySeconds(20) //有效时长,单位是秒
.tokenRepository(persistentTokenRepository())
.userDetailsService(userDetailsService)
3. 页面添加记住我复选框
<br>
记住我:<input type="checkbox"name="remember-me"title="记住密码"/>
此处:name 属性值必须位 remember-me,不能改为其他值。
4. 使用lucy进行登录测试
登录成功之后,关闭浏览器再次访问 http://localhost:8111/test/index,发现依然可以使用!
5. 基本原理说明
本质原理图:
SpringSecurity的封装图:
9、用户注销
1. 在成功页添加退出链接
<body>
登录成功<br> <a href="/logout">退出</a>
</body>
2. 在配置类中添加退出映射地址
//退出登录
http.logout().logoutUrl("/logout").logoutSuccessUrl("/test/hello").permitAll();
3. 测试
登录成功之后,在成功页面点击退出,在去访问其他controller不能进行访问的。
10、CSRF
1. CSRF 理解
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法。跟跨网站脚本(XSS)相比,XSS利用的是用户对指定网站的信任,CSRF 利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。
2. 案例
在登录页面添加一个隐藏域:
<input type="hidden"th:if="${_csrf}!=null"th:value="${_csrf.token}"name="_csrf"/>
关闭安全配置的类中的 csrf
// http.csrf().disable();
如果有收获!!! 希望老铁们来个三连,点赞、收藏、转发。
创作不易,别忘点个赞,可以让更多的人看到这篇文章,顺便鼓励我写出更好的博客