目录
一、UserDetailsService接口
当什么也没有配置的时候,账号和密码是由 Spring Security 定义生成的。在查看认证过程源码的过程中也发现要关联UserDetailService来完成认证操作。而在实际项目中账号和密码都是从数据库中查询出来的。 所以我们要通过自定义逻辑控制认证逻辑。
UserDetailsService 接口的方法:
public interface UserDetailsService {
//根据用户名获取用户的详细信息
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
UserDetailsService实现类的返回值是UserDetails类型的,该接口中的如下:
public interface UserDetails extends Serializable {
// 表示获取登录用户所有权限
Collection<? extends GrantedAuthority> getAuthorities();
// 表示获取密码
String getPassword();
// 表示获取用户名
String getUsername();
// 表示判断账户是否过期
boolean isAccountNonExpired();
// 表示判断账户是否被锁定
boolean isAccountNonLocked();
// 表示凭证{密码}是否过期
boolean isCredentialsNonExpired();
// 表示当前用户是否可用
boolean isEnabled();
}
UserDetails的典型实现类-User
public class User implements UserDetails, CredentialsContainer {
... ...
public User(String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(username, password, true, true, true, true, authorities);
}
//参数:名字、密码、权限的集合
//注:客户端传来的数据名字应该也是username、password
... ...
}
二、PasswordEncoder接口
public interface PasswordEncoder {
String encode(CharSequence rawPassword);
// 表示把参数按照特定的解析规则进行解析
boolean matches(CharSequence rawPassword, String encodedPassword);
// 表示验证从存储中获取的编码密码与编码后提交的原始密码是否匹配。如果密码匹
//配,则返回 true;如果不匹配,则返回 false。第一个参数表示需要被解析的密码。第二个
//参数表示存储的密码。
default boolean upgradeEncoding(String encodedPassword) {return false;}
// 表示如果解析的密码能够再次进行解析且达到更安全的结果则返回 true,否则返回
//false。默认返回 false。
}
BCryptPasswordEncoder 是对 bcrypt 强散列方法的具体实现。是基于 Hash 算法实现的单 向加密。可以通过 strength 控制加密强度,默认 10.
测试:
@Test
void testEncoder(){
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
String text= "hello,wz!";
//加密
String encodedText = encoder.encode(text);
System.out.println("加密后:"+encodedText);
//判断加密前后数据是否一致
System.out.println(encoder.matches(text, encodedText));
}
结果:
加密后:$2a$10$BTmx9NhVz9LbqXboK3aTXOm7QDyFzXdlNiBMWg1.2L0MRMqqAmuEe
true
三、从数据库认证实现登录
3.1 数据库表
//用户表 (1,'wz',123456)(2,'zp',123456)
create table users(
id bigint primary key auto_increment,
username varchar(20) unique not null,
password varchar(100)
);
//角色表 (1,'admin')(2,'user')
create table role(
id bigint primary key auto_increment,
name varchar(20)
);
//用户与角色绑定 (1,1)(2,2)
create table role_user(
uid bigint,
rid bigint
);
//权限表 (1,'insert')(2,'delete')(3,'update')(4,'select')
create table auth(
id bigint primary key auto_increment,
name varchar(20)
);
//权限与角色绑定 (1,1)(1,2)(1,3)(1,4)(2,4)
create table role_auth(
rid bigint,
aid bigint
);
3.2 实体类
@Data
public class Users {
private Integer id;
private String username;
private String password;
}
3.3 Mapper、数据库配置
//@MapperScan("com.wz.springsecurity_web.mapper") 注意!此注解加在springboot启动类上
@Repository
public interface UsersMapper extends BaseMapper<Users> {
String selectUserRole(String username);//获取角色
List<String> selectUserAuths(String username);//获取权限
}
<?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.wz.springsecurity_web.mapper.UsersMapper">
<select id="selectUserRole" resultType="java.lang.String">
SELECT r.name role FROM users u,role_user ru, role r
WHERE u.`id`=ru.`uid`
AND r.`id`=ru.`rid`
AND u.`username`=#{username}
</select>
<select id="selectUserAuths" resultType="java.lang.String">
SELECT a.name auth FROM users u,role_user ru, role r,role_auth ra,auth a
WHERE u.`id`=ru.`uid`
AND r.`id`=ru.`rid`
AND r.`id`=ra.`rid`
AND a.`id`=ra.`aid`
AND u.`username`=#{username}
</select>
</mapper>
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring_security
username: root
password: 310333
3.4 UserDetailsService实现类
@Service
public class MyUserDetailsService implements UserDetailsService {
@Autowired
private UsersMapper usersMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<Users> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
Users users = usersMapper.selectOne(wrapper);//根据名字去数据库查询,看用户是否存在
if (users == null) throw new UsernameNotFoundException("用户名不存在!");
else {
String role = usersMapper.selectUserRole(username);//获取角色
List<String> auths = usersMapper.selectUserAuths(username);//获取权限
List<GrantedAuthority> userAuths = new ArrayList<>();//创建一个存放权限与角色的集合
userAuths.add(new SimpleGrantedAuthority("ROLE_" + role));//把角色加到集合中
for (String auth : auths) userAuths.add(new SimpleGrantedAuthority(auth));//把权限加到集合中
return new User(users.getUsername(), new BCryptPasswordEncoder().encode(users.getPassword()), userAuths);
}
}
}
3.5 主配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
//注入 PasswordEncoder 类到 spring 容器中
@Bean
PasswordEncoder passwordEncoder(){return new BCryptPasswordEncoder();}
//指定密码的编码器
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin(); // 表单登录
http.authorizeRequests() //认证配置
.anyRequest() // 任何请求
.authenticated(); // 都需要身份验证
}
}
接下来在浏览器中输入 localhost:8080/hello ,输入数据库中的账户和密码就成功登录了。
四、登录的相关配置
我们可以在配置类中设置:
登陆页面的资源路径
登录请求路径
登录成功的跳转路径
指定哪些路径不被拦截
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin() // 表单登录
.loginPage("/login.html")//登录页面路径
.loginProcessingUrl("/user/login")//登录请求路径
.defaultSuccessUrl("/index").permitAll();//登陆成功跳转路径
http.authorizeRequests() //认证配置
.antMatchers("/hello","/user/login").permitAll() //指定的路径无需拦截
.anyRequest() // 其他任何请求
.authenticated(); // 需要身份验证
http.csrf().disable();//关闭csrf
}
login.html(resourse/static内)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登录</title>
</head>
<body>
<form action="/user/login" method="post">
user: <input type="text" name="username"/><br/>
password:<input type="text" name="password"/><br/>
<input type="submit" value="login"/>
</form>
</body>
</html>
index.html(resourse/templates内)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>首页</title>
</head>
<body>
<div>您已登录成功!欢迎来到首页!</div>
</body>
</html>
控制器方法
@GetMapping("/index")
public String index(){
return "index";
}
这样一来,我们想要进入index.html的话会自动跳转至我们自定义的登录页面login.html,登录成功后跳转回index.html
五、角色、权限的认证配置
hasAuthority- 需要某个权限才可以访问
hasAnyAuthority- 满足单个权限就可访问
hasAnyRole- 需要某个角色才可以访问
hasRole- 满足单个角色就可访问
还是在配置类中配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
... ...
http.authorizeRequests()
.antMatchers("/hello","/user/login").permitAll()//不需要验证的请求路径
.antMatchers("/index").hasAnyAuthority("select","delete")//包含任何一个权限就可以访问这个路径
//.antMatchers("/index").hasRole("admin")//当前主体要提供这个角色才可以访问
... ...
如果不满足条件,会返回403的错误。
六、自定义403页面
在配置类中配置:
@Override
protected void configure(HttpSecurity http) throws Exception {
... ...
http.exceptionHandling().accessDeniedPage("/unauth.html");//访问被拒绝时跳转的页面
... ...
unauth.html(resources/static)
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>警告</title>
</head>
<body>
<div align="center"><h1>没有访问权限!</h1></div>
</body>
</html>
七、注解的使用
@Secured:
判断是否具有角色,另外需要注意的是这里匹配的字符串需要添加前缀“ROLE_“。
首先在springboot启动类上加注解
@EnableGlobalMethodSecurity(securedEnabled=true)
@Secured({"ROLE_admin","ROLE_user"})
@GetMapping("/index")
public String index(){
return "index";
}
@PreAuthorize:
进入方法前的权限验证。
首先在springboot启动类上加注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
@PreAuthorize("hasAnyAuthority('select','delete')")
@GetMapping("/index")
public String index(){
return "index";
}
@PostAuthorize:
方法执行后再进行权限验证.
首先在springboot启动类上加注解
@EnableGlobalMethodSecurity(prePostEnabled = true)
还有@PostFilter、@PreFilter注解,可以对数据过滤 ,如
@PostFilter("filterObject.username == 'admin1'")
@PreFilter(value = "filterObject.id%2==0")
八、基于数据库的remeberMe
创建数据库表
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;
修改主配置类
... ...
@Autowired
DataSource dataSource;//注入数据源
@Bean
PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl jdbcTokenRepository=new JdbcTokenRepositoryImpl();
jdbcTokenRepository.setDataSource(dataSource);// 赋值数据源
jdbcTokenRepository.setCreateTableOnStartup(false);//自动创建表,第一次执行会创建,以后要执行就要删除掉!
return jdbcTokenRepository;
}
... ...
@Override
protected void configure(HttpSecurity http) throws Exception {
... ...
http.rememberMe().tokenRepository(persistentTokenRepository())
.tokenValiditySeconds(60)//设置有效时长(s)
.userDetailsService(userDetailsService);
... ...
在登陆页login.html的表单中添加“记住我”选项
记住我:<input type="checkbox"name="remember-me"title="记住密码"/><br/>
这样登录之后即使关闭浏览器,短时间内也可以不用重新登录
九、注销
在index.html添加一个退出链接
<a href="/logout">退出</a>
在配置类中添加退出映射地址
@Override
protected void configure(HttpSecurity http) throws Exception {
... ...
http.logout().logoutUrl("/logout").logoutSuccessUrl("/index").permitAll
... ...
退出之后就需要重新登录了
十、CSRF
跨站请求伪造(英语:Cross-site request forgery)是一种挟制用户在当前已登录的 Web 应用程序上执行非本意的操作的攻击方法,利用的是网站对用户网页浏览器的信任。
跨站请求攻击,简单地说,是攻击者通过一些技术手段欺骗用户的浏览器去访问一个 自己曾经认证过的网站并运行一些操作(如发邮件,发消息,甚至财产操作如转账和购买 商品)。由于浏览器曾经认证过,所以被访问的网站会认为是真正的用户操作而去运行。 这利用了 web 中用户身份验证的一个漏洞:简单的身份验证只能保证请求发自某个用户的浏览器,却不能保证请求本身是用户自愿发出的。
从 Spring Security 4.0 开始,默认情况下会启用 CSRF 保护,以防止 CSRF 攻击应用 程序,Spring Security CSRF 会针对 PATCH,POST,PUT 和 DELETE 方法进行防护。
如何实现的?
1. 生成 csrfToken 保存到 HttpSession 或者 Cookie 中。
2. 请求到来时,从请求中提取 csrfToken,和保存的 csrfToken 做比较,进而判断当前请求是否合法。主要通过 CsrfFilter 过滤器来完成。
若要开启这个功能,要从配置类中删除字段:
// http.csrf().disable();
在登录页面添加一个隐藏域:
<input type="hidden"th:if="${_csrf}!=null"th:value="${_csrf.token}"name="_csrf"/>