原理:内部其实是过滤器链
当用户登录发起认证请求时,会通过UsernamePasswordAuthenticationFilter
进行用户认证,认证成功之后,SpringSecurity 调用前期配置好的记住我功能,实际是调用了RememberMeService
接口,其接口的实现类会将用户的信息生成Token并将它写入 response 的Cookie中,在写入的同时,内部的TokenRepositoryTokenRepository
会将这份Token再存入数据库一份。
当用户再次访问服务器资源的时候,首先会经过RememberMeAuthenticationFiler
过滤器,在这个过滤器里面会读取当前请求中携带的 Cookie,这里存着上次服务器保存 的Token,然后去数据库中查找是否有相应的 Token,如果有,则再通过UserDetailsService
获取用户的信息。
记住我功能的过滤器
从图中可以得知记住我的过滤器在过滤链的中部,注意是在UsernamePasswordAuthenticationFilter
之后。
在 html 中增加记住我
复选框checkbox控件,注意其中复选框的name 一定必须为remember-me
<input type="checkbox" name="remember-me" value="true"/>
注册时密码要进行加密后保存
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
// 密码加密
userDTO.setPassword(bCryptPasswordEncoder.encode(userDTO .getPassword()));
xxxmapper.save(userDTO);
编码 - spring boot搭建项目
引入spring-boot-starter-security依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
1,用户 User 类,对应数据库user表的字段
@Data
public class User implements Serializable{
private static final long serialVersionUID = 8091197205740959431L;
private Long id;
@NotBlank(message = "请输入昵称")
@Length(max = 30,message = "姓名不能超过30个字符")
private String nickname;
@NotBlank(message = "请输入用户名")
@Length(max = 30,message = "用户名不能超过30个字符")
private String username;
@NotBlank(message = "请输入密码")
private String password;
@Email(message = "请输入正确的邮箱格式")
private String email;
private String phoneNumber;
private String address;
private String avatar;
private Boolean isManager;
private Integer level;
private Date createTime;
private Date updateTime;
private String token;
private Date lastLoginTime;
}
用户扩展类,后面使用 UserDTO 类操作,必须实现 UserDetails
@Data
public class UserDTO extends User implements UserDetails{
//这四个暂且都给默认值为true
private boolean isAccountNonExpired=true;
private boolean isAccountNonLocked=true;
private boolean isCredentialsNonExpired=true;
private boolean isEnabled=true;
//存储权限的集合
private List<GrantedAuthority> authorities;
}
2,自定义用户信息服务UserDetailsService
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserMapper userMapper;
/**
* 根据用户名查找到用户信息,然后返回。Spring Security会处理验证密码是否正确的逻辑
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//定义存储权限的列表
List<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>();
//通过用户名找到用户
UserDTO userDTO = userMapper.findByUsername(username);
if (userDTO != null) {
//通过用户名找到权限
List<Permission> perms = userMapper.listPermissionByUserId(userDTO.getId());
for (Permission p : perms) {
authorities.add(new SimpleGrantedAuthority(p.getName()));
}
userDTO.setAuthorities(authorities);
}
return userDTO;
}
}
3,自定义security的配置类SecurityConfig,必须继承WebSecurityConfigurerAdapter(核心配置类)
package com.article.config;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
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.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.firewall.DefaultHttpFirewall;
import org.springframework.security.web.firewall.HttpFirewall;
import com.article.service.impl.CustomUserDetailsService;
@Configuration
// @EnableWebSecurity ※spring boot自动装配,此注解可以不用写
// @EnableGlobalMethodSecurity(prePostEnabled = true) ※基于方法授权[注解]的时候,需要开启此注解
public class SecurityConfig extends WebSecurityConfigurerAdapter{
@Autowired
private DataSource dataSource;
@Autowired
private CustomUserDetailsService userDetailsService;
//用户登录成功后token存活秒数
@Value("${token.ValiditySeconds}")
private Integer tokenValiditySeconds;
//密码使用 BCryptPasswordEncoder 加密
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//允许多请求地址多加斜杠 比如 /msg/list //msg/list
//因为报这个错误,所以加上了这个方法[org.springframework.security.web.firewall.RequestRejectedException]
@Bean
public HttpFirewall httpFirewall() {
return new DefaultHttpFirewall();
}
//记住我功能的相关配置
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
// 将 DataSource 设置到 PersistentTokenRepository
tokenRepository.setDataSource(dataSource);
// 第一次启动的时候自动建表,以后就不能加这行代码了(可以不用这句话,自己手动建表,源码中有语句)
// persistentTokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
//安全拦截机制。 ■此授权是基于web授权的方式,要注意写时候的顺序
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/*","/1120/toLogin").permitAll() // 所有 /* 请求和 /1120/toLogin 请求都可以访问
.antMatchers("/add").hasAuthority("add") // add请求必须拥有add权限
.antMatchers("/1120/**").authenticated() // 所有/1120/**请求必须通过认证
.anyRequest().permitAll() //其他请求可以访问。 ⇒不能放在最前面
.and()
.formLogin() //允许表单登录
.loginPage("/myLogin").permitAll() //自定义登录的url
.usernameParameter("username") //自定义登录form的用户名的name
.passwordParameter("password") //自定义登录form的密码的name
.loginProcessingUrl("/actionLogin") //自定义登录form的action名字。※点击登录按钮,spring security会按照上面自定义的UserDetailsService去处理登录验证
.defaultSuccessUrl("/defaultSuccessUrl", true) //登陆成功后跳转的url,可以做 session 和 cookie 等的处一些理
//.successHandler(customizeSuccessHandler) 使用 successHandler 后,登录成功后不能自动跳转到指定url [自己测试的]
//.successForwardUrl("/index") //登陆成功后跳转的url,但是地址栏不变。(回退会导致表单重复提交)
//.defaultSuccessUrl("/index") //此跳转测试无效
.failureUrl("/loginFail") //登陆失败后跳转的url
.and()
.exceptionHandling().accessDeniedPage("/403") //没有权限访问时候跳转的url
.and()
.logout()
.logoutUrl("/1120/myLogout") //自定义注销路径
.logoutSuccessUrl("/myLogout") //注销成功后跳转的url。[可以自己再次清楚session或设置cookie之类的一些处理,最后再跳转到登录页面]
.and()
.sessionManagement().invalidSessionUrl("/1120/toLogin") //session失效跳转的url
.and()
.rememberMe() //记住我
.tokenRepository(persistentTokenRepository()) // 配置数据库源
.tokenValiditySeconds(tokenValiditySeconds) //token过期时间
.userDetailsService(userDetailsService)
.csrf().disable() //关闭csrf
;
}
登录成功后跳转到的 URL
@GetMapping("/defaultSuccessUrl")
public String toIndex(HttpServletRequest request,HttpServletResponse response,Model model) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//获取登录用户信息
UserDTO user = (UserDTO) authentication.getPrincipal();
//-------------- 下面这块是之前没用"记住我"功能时候,自己写入和更新用户表的token -----------------//
//清空密码
user.setPassword(null);
//存cookie
String token = UUID.randomUUID().toString();
userService.updateTokenById(token, user.getId());
Cookie cookie = new Cookie("token", token);
cookie.setMaxAge(24 * 60 * 60);
response.addCookie(cookie);
//-------------- 上面这块是之前没用"记住我"功能时候,自己写入和更新用户表的token -----------------//
//存session
request.getSession().setAttribute("user", user);
//查询最后一次登录时间
Date lastLoginTime = user.getLastLoginTime();
model.addAttribute("lastLoginTime", lastLoginTime);
//更新最后一次登陆时间为现在的时间
userService.updateLastLoginTimeById(user.getId());
return "admin/index";
}
注销成功后跳转到的 URL ★之前没用spring security时候,自己实现的注销,清除session和cookie
@GetMapping("/myLogout")
public String logout(HttpSession session, HttpServletResponse response) {
session.removeAttribute("user");
Cookie cookie = new Cookie("token", null);
cookie.setMaxAge(0);
cookie.setPath("/**");
response.addCookie(cookie);
return "redirect:/1120/toLogin";
}
用户名或密码错误
<!-- th:if="${param.error}" 固定写法-->
<div th:if="${param.error}" th:text="用户名或密码错误"></div>
■基于方法授权的方式【使用注解】
public interface IndexController {
@GetMapping("/index")
@PreAuthorize("isAnonymous()")
public String index(){
...
}
@GetMapping("/save")
@PreAuthorize("hasAuthority("p1"))
public String save(){
...
}
@GetMapping("/delete")
@PreAuthorize("hasAuthority('p_transfer') and hasAuthority('p_read_account')")
public String delete(){
...
}
}
以上配置标明index方法可匿名访问,save方法需要p1权限才可以访问,delete方法需要同时拥有p_transfer和p_read_account 权限才能访问,底层使用WebExpressionVoter投票器,可从AffirmativeBased第23行代码跟踪。
会话
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保存在会话中。spring security提供会话管 理,认证通过后将身份信息放入SecurityContextHolder上下文,SecurityContext与当前线程进行绑定,方便获取 用户身份。
/**
* 获取当前登录用户名
* @return
*/
private String getUsername(){
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
//判断是否已经认证
if(!authentication.isAuthenticated()){
return null;
}
Object principal = authentication.getPrincipal();
String username = null;
if (principal instanceof org.springframework.security.core.userdetails.UserDetails) {
username =((org.springframework.security.core.userdetails.UserDetails)principal).getUsername();
}else {
username = principal.toString();
}
return username;
}
会话超时
可以再sevlet容器中设置Session的超时时间,如下设置Session有效期为3600s;
spring boot 配置文件:
server.servlet.session.timeout=3600s
安全会话cookie
我们可以使用httpOnly和secure标签来保护我们的会话cookie:
-
httpOnly:如果为true,那么浏览器脚本将无法访问cookie
-
secure:如果为true,则cookie将仅通过HTTPS连接发送
spring boot 配置文件:
server.servlet.session.cookie.http‐only=true
server.servlet.session.cookie.secure=true
★ 结合thymeleaf中的一些功能
maven依赖:
<dependency>
<groupId>org.thymeleaf.extras</groupId>
<artifactId>thymeleaf-extras-springsecurity5</artifactId>
<version>3.0.4.RELEASE</version>
</dependency>
导入命名空间:
xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5"
案例:
<!--登录注销-->
<div class="right menu">
<!--如果未登录-->
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/antionLogin}">
<i class="address card icon"></i> 登录
</a>
</div>
<!--如果已登录-->
<div sec:authorize="isAuthenticated()">
<a class="item">
<i class="address card icon"></i>
用户名:<span sec:authentication="principal.username"></span>
角色:<span sec:authentication="principal.authorities"></span>
</a>
</div>
<div sec:authorize="isAuthenticated()">
<a class="item" th:href="@{/logout}">
<i class="address card icon"></i> 注销
</a>
</div>
</div>