基于 Spring Security 的前后端分离的权限控制系统
话不多说,入正题。一个简单的权限控制系统需要考虑的问题如下:
- 权限如何加载
- 权限匹配规则
- 登录
1. 引入maven依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.1</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo5</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo5</name>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.76</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>commons-codec</groupId>
<artifactId>commons-codec</artifactId>
<version>1.15</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
application.properties配置
server.port=8080
server.servlet.context-path=/demo
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/demo?useUnicode=true&characterEncoding=utf8
spring.datasource.username=root
spring.datasource.password=123456
spring.jpa.database=mysql
spring.jpa.open-in-view=true
spring.jpa.properties.hibernate.enable_lazy_load_no_trans=true
spring.jpa.show-sql=true
spring.redis.host=192.168.28.31
spring.redis.port=6379
spring.redis.password=123456
2. 建表并生成相应的实体类
SysUser.java
package com.example.demo5.entity;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.Set;
/**
* 用户表
* @Author ChengJianSheng
* @Date 2021/6/12
*/
@Setter
@Getter
@Entity
@Table(name = "sys_user")
public class SysUserEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Integer id;
@Column(name = "username")
private String username;
@Column(name = "password")
private String password;
@Column(name = "mobile")
private String mobile;
@Column(name = "enabled")
private Integer enabled;
@Column(name = "create_time")
private LocalDate createTime;
@Column(name = "update_time")
private LocalDate updateTime;
@OneToOne
@JoinColumn(name = "dept_id")
private SysDeptEntity dept;
@ManyToMany
@JoinTable(name = "sys_user_role",
joinColumns = {@JoinColumn(name = "user_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")})
private Set<SysRoleEntity> roles;
}
SysDept.java
部门相当于用户组,这里简化了一下,用户组没有跟角色管理
package com.example.demo5.entity;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;
/**
* 部门表
* @Author ChengJianSheng
* @Date 2021/6/12
*/
@Data
@Entity
@Table(name = "sys_dept")
public class SysDeptEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Integer id;
/**
* 部门名称
*/
@Column(name = "name")
private String name;
/**
* 父级部门ID
*/
@Column(name = "pid")
private Integer pid;
// @ManyToMany(mappedBy = "depts")
// private Set<SysRoleEntity> roles;
}
SysMenu.java
菜单相当于权限
package com.example.demo5.entity;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;
/**
* 菜单表
* @Author ChengJianSheng
* @Date 2021/6/12
*/
@Setter
@Getter
@Entity
@Table(name = "sys_menu")
public class SysMenuEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Integer id;
/**
* 资源编码
*/
@Column(name = "code")
private String code;
/**
* 资源名称
*/
@Column(name = "name")
private String name;
/**
* 菜单/按钮URL
*/
@Column(name = "url")
private String url;
/**
* 资源类型(1:菜单,2:按钮)
*/
@Column(name = "type")
private Integer type;
/**
* 父级菜单ID
*/
@Column(name = "pid")
private Integer pid;
/**
* 排序号
*/
@Column(name = "sort")
private Integer sort;
@ManyToMany(mappedBy = "menus")
private Set<SysRoleEntity> roles;
}
SysRole.java
package com.example.demo5.entity;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.io.Serializable;
import java.util.Set;
/**
* 角色表
* @Author ChengJianSheng
* @Date 2021/6/12
*/
@Setter
@Getter
@Entity
@Table(name = "sys_role")
public class SysRoleEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
@Column(name = "id")
private Integer id;
/**
* 角色名称
*/
@Column(name = "name")
private String name;
@ManyToMany(mappedBy = "roles")
private Set<SysUserEntity> users;
@ManyToMany
@JoinTable(name = "sys_role_menu",
joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
inverseJoinColumns = {@JoinColumn(name = "menu_id", referencedColumnName = "id")})
private Set<SysMenuEntity> menus;
// @ManyToMany
// @JoinTable(name = "sys_dept_role",
// joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},
// inverseJoinColumns = {@JoinColumn(name = "dept_id", referencedColumnName = "id")})
// private Set<SysDeptEntity> depts;
}
注意,不要使用@Data注解,因为@Data包含@ToString注解
不要随便打印SysUser,例如:System.out.println(sysUser); 任何形式的toString()调用都不要有,否则很有可能造成循环调用,死递归。想想看,SysUser里面要查SysRole,SysRole要查SysMenu,SysMenu又要查SysRole。除非不用懒加载。
3. 自定义UserDetails
虽然可以使用Spring Security自带的User,但是笔者还是强烈建议自定义一个UserDetails,后面可以直接将其序列化成json缓存到redis中
package com.example.demo5.domain;
import lombok.Setter;
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 java.util.Collection;
import java.util.Set;
/**
* @Author ChengJianSheng
* @Date 2021/6/12
* @see User
* @see org.springframework.security.core.userdetails.User
*/
@Setter
public class MyUserDetails implements UserDetails {
private String username;
private String password;
private boolean enabled;
// private Collection<? extends GrantedAuthority> authorities;
private Set<SimpleGrantedAuthority> authorities;
public MyUserDetails(String username, String password, boolean enabled, Set<SimpleGrantedAuthority> authorities) {
this.username = username;
this.password = password;
this.enabled = enabled;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
}
都自定义UserDetails了,当然要自己实现UserDetailsService了。这里当时偷懒直接用自带的User,后面放缓存的时候才知道不方便。
package com.example.demo5.service;
import com.example.demo5.entity.SysMenuEntity;
import com.example.demo5.entity.SysRoleEntity;
import com.example.demo5.entity.SysUserEntity;
import com.example.demo5.repository.SysUserRepository;
import org.apache.commons.lang3.StringUtils;
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 javax.annotation.Resource;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @Author ChengJianSheng
* @Date 2021/6/12
*/
@Service
public class MyUserDetailsService implements UserDetailsService {
@Resource
private SysUserRepository sysUserRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUserEntity sysUserEntity = sysUserRepository.findByUsername(username);
Set<SysRoleEntity> roleSet = sysUserEntity.getRoles();
Set<SimpleGrantedAuthority> authorities = roleSet.stream().flatMap(role->role.getMenus().stream())
.filter(menu-> StringUtils.isNotBlank(menu.getCode()))
.map(SysMenuEntity::getCode)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
User user = new User(sysUserEntity.getUsername(), sysUserEntity.getPassword(), authorities);
return user;
}
}
算了,还是改过来吧
package com.example.demo5.service;
import com.example.demo5.domain.MyUserDetails;
import com.example.demo5.entity.SysMenuEntity;
import com.example.demo5.entity.SysRoleEntity;
import com.example.demo5.entity.SysUserEntity;
import com.example.demo5.repository.SysUserRepository;
import org.apache.commons.lang3.StringUtils;
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 javax.annotation.Resource;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @Author ChengJianSheng
* @Date 2021/6/12
*/
@Service
public class MyUserDetailsService implements UserDetailsService {
@Resource
private SysUserRepository sysUserRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUserEntity sysUserEntity = sysUserRepository.findByUsername(username);
Set<SysRoleEntity> roleSet = sysUserEntity.getRoles();
Set<SimpleGrantedAuthority> authorities = roleSet.stream().flatMap(role->role.getMenus().stream())
.filter(menu-> StringUtils.isNotBlank(menu.getCode()))
.map(SysMenuEntity::getCode)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
// return new User(sysUserEntity.getUsername(), sysUserEntity.getPassword(), authorities);
return new MyUserDetails(sysUserEntity.getUsername(), sysUserEntity.getPassword(), 1==sysUserEntity.getEnabled(), authorities);
}
}
4. 自定义各种Handler
登录成功
package com.example.demo5.handler;
import com.alibaba.fastjson.JSON;
import com.example.demo5.domain.MyUserDetails;
import com.example.demo5.domain.RespResult;
import com.example.demo5.util.JwtUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
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;
import java.util.concurrent.TimeUnit;
/**
* 登录成功
*/
@Component
public class MyAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private static ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException {
MyUserDetails user = (MyUserDetails) authentication.getPrincipal();
String username = user.getUsername();
String token = JwtUtils.createToken(username);
stringRedisTemplate.opsForValue().set("TOKEN:" + token, JSON.toJSONString(user), 60, TimeUnit.MINUTES);
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(new RespResult<>(1, "success", token)));
writer.flush();
writer.close();
}
}
登录失败
package com.example.demo5.handler;
import com.example.demo5.domain.RespResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
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 MyAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(new RespResult<>(0, exception.getMessage(), null)));
writer.flush();
writer.close();
}
}
未登录
package com.example.demo5.handler;
import com.example.demo5.domain.RespResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
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;
/**
* 未认证(未登录)统一处理
* @Author ChengJianSheng
* @Date 2021/5/7
*/
@Component
public class MyAuthenticationEntryPoint implements AuthenticationEntryPoint {
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(new RespResult<>(0, "未登录,请先登录", null)));
writer.flush();
writer.close();
}
}
未授权
package com.example.demo5.handler;
import com.example.demo5.domain.RespResult;
import com.fasterxml.jackson.databind.ObjectMapper;
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 MyAccessDeniedHandler implements AccessDeniedHandler {
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(new RespResult<>(0, "抱歉,您没有权限访问", null)));
writer.flush();
writer.close();
}
}
Session过期
package com.example.demo5.handler;
import com.example.demo5.domain.RespResult;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class MyExpiredSessionStrategy implements SessionInformationExpiredStrategy {
private static ObjectMapper objectMapper = new ObjectMapper();
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
String msg = "登录超时或已在另一台机器登录,您被迫下线!";
RespResult respResult = new RespResult(0, msg, null);
HttpServletResponse response = event.getResponse();
response.setContentType("application/json;charset=utf-8");
PrintWriter writer = response.getWriter();
writer.write(objectMapper.writeValueAsString(respResult));
writer.flush();
writer.close();
}
}
退出成功
package com.example.demo5.handler;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
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 MyLogoutSuccessHandler implements LogoutSuccessHandler {
private static ObjectMapper objectMapper = new ObjectMapper();
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String token = request.getHeader("token");
stringRedisTemplate.delete("TOKEN:" + token);
response.setContentType("application/json;charset=utf-8");
PrintWriter printWriter = response.getWriter();
printWriter.write(objectMapper.writeValueAsString("logout success"));
printWriter.flush();
printWriter.close();
}
}
5. Token处理
现在由于前后端分离,服务端不再维持Session,于是需要token来作为访问凭证
token工具类
package com.example.demo5.util;
import io.jsonwebtoken.*;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
/**
* @Author ChengJianSheng
* @Date 2021/5/7
*/
public class JwtUtils {
private static long TOKEN_EXPIRATION = 24 * 60 * 60 * 1000;
private static String TOKEN_SECRET_KEY = "123456";
/**
* 生成Token
* @param subject 用户名
* @return
*/
public static String createToken(String subject) {
long currentTimeMillis = System.currentTimeMillis();
Date currentDate = new Date(currentTimeMillis);
Date expirationDate = new Date(currentTimeMillis + TOKEN_EXPIRATION);
// 存放自定义属性,比如用户拥有的权限
Map<String, Object> claims = new HashMap<>();
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(currentDate)
.setExpiration(expirationDate)
.signWith(SignatureAlgorithm.HS512, TOKEN_SECRET_KEY)
.compact();
}
public static String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public static boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
public static Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public static <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private static Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(TOKEN_SECRET_KEY).parseClaimsJws(token).getBody();
}
}
前后端约定登录成功以后,将token放到header中。于是,我们需要过滤器来处理请求Header中的token,为此定义一个TokenFilter
package com.example.demo5.filter;
import com.alibaba.fastjson.JSON;
import com.example.demo5.domain.MyUserDetails;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
/**
* @Author ChengJianSheng
* @Date 2021/6/17
*/
@Component
public class TokenFilter extends OncePerRequestFilter {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String token = request.getHeader("token");
System.out.println("请求头中带的token: " + token);
String key = "TOKEN:" + token;
if (StringUtils.isNotBlank(token)) {
String value = stringRedisTemplate.opsForValue().get(key);
if (StringUtils.isNotBlank(value)) {
// String username = JwtUtils.extractUsername(token);
MyUserDetails user = JSON.parseObject(value, MyUserDetails.class);
if (null != user && null == SecurityContextHolder.getContext().getAuthentication()) {
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
// 刷新token
// 如果生存时间小于10分钟,则再续1小时
long time = stringRedisTemplate.getExpire(key);
if (time < 600) {
stringRedisTemplate.expire(key, (time + 3600), TimeUnit.SECONDS);
}
}
}
}
chain.doFilter(request, response);
}
}
token过滤器做了两件事,一是获取header中的token,构造UsernamePasswordAuthenticationToken放入上下文中。权限可以从数据库中再查一遍,也可以直接从之前的缓存中获取。二是为token续期,即刷新token。
由于我们采用jwt生成token,因此没法中途更改token的有效期,只能将其放到Redis中,通过更改Redis中key的生存时间来控制token的有效期。
6. 访问控制
首先来定义资源
package com.example.demo5.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Author ChengJianSheng
* @Date 2021/6/12
*/
@RestController
@RequestMapping("/hello")
public class HelloController {
@PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHello')")
@GetMapping("/sayHello")
public String sayHello() {
return "hello";
}
@PreAuthorize("@myAccessDecisionService.hasPermission('hello:sayHi')")
@GetMapping("/sayHi")
public String sayHi() {
return "hi";
}
}
资源的访问控制我们通过判断是否有相应的权限字符串
package com.example.demo5.service;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.util.Set;
import java.util.stream.Collectors;
@Component("myAccessDecisionService")
public class MyAccessDecisionService {
public boolean hasPermission(String permission) {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
Object principal = authentication.getPrincipal();
if (principal instanceof UserDetails) {
UserDetails userDetails = (UserDetails) principal;
// SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
Set<String> set = userDetails.getAuthorities().stream().map(GrantedAuthority::getAuthority).collect(Collectors.toSet());
return set.contains(permission);
}
return false;
}
}
7. 配置WebSecurity
package com.example.demo5.config;
import com.example.demo5.filter.TokenFilter;
import com.example.demo5.handler.*;
import com.example.demo5.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @Author ChengJianSheng
* @Date 2021/6/12
*/
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Autowired
private TokenFilter tokenFilter;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin()
// .usernameParameter("username")
// .passwordParameter("password")
// .loginPage("/login.html")
.successHandler(myAuthenticationSuccessHandler)
.failureHandler(myAuthenticationFailureHandler)
.and()
.logout().logoutSuccessHandler(new MyLogoutSuccessHandler())
.and()
.authorizeRequests()
.antMatchers("/demo/login").permitAll()
// .antMatchers("/css/**", "/js/**", "/**/images/*.*").permitAll()
// .regexMatchers(".+[.]jpg").permitAll()
// .mvcMatchers("/hello").servletPath("/demo").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.accessDeniedHandler(new MyAccessDeniedHandler())
.authenticationEntryPoint(new MyAuthenticationEntryPoint())
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.maximumSessions(1)
.maxSessionsPreventsLogin(false)
.expiredSessionStrategy(new MyExpiredSessionStrategy());
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
http.csrf().disable();
}
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
public static void main(String[] args) {
System.out.println(new BCryptPasswordEncoder().encode("123456"));
}
}
注意,我们将自定义的TokenFilter放到UsernamePasswordAuthenticationFilter之前
所有过滤器的顺序可以查看 org.springframework.security.config.annotation.web.builders.FilterComparator 或者 org.springframework.security.config.annotation.web.builders.FilterOrderRegistration
8. 看效果
9. 补充:手机号+短信验证码登录
参照org.springframework.security.authentication.UsernamePasswordAuthenticationToken写一个短信认证Token
package com.example.demo5.filter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.util.Assert;
import java.util.Collection;
/**
* @Author ChengJianSheng
* @Date 2021/5/12
*/
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
private final Object principal;
private Object credentials;
public SmsCodeAuthenticationToken(Object principal, Object credentials) {
super(null);
this.principal = principal;
this.credentials = credentials;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return credentials;
}
@Override
public Object getPrincipal() {
return principal;
}
@Override
public void setAuthenticated(boolean authenticated) {
Assert.isTrue(!authenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
super.setAuthenticated(false);
}
@Override
public void eraseCredentials() {
super.eraseCredentials();
}
}
参照org.springframework.security.authentication.dao.DaoAuthenticationProvider写一个自己的短信认证Provider
package com.example.demo5.filter;
import com.example.demo.service.MyUserDetailsService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
/**
* @Author ChengJianSheng
* @Date 2021/5/12
*/
public class SmsAuthenticationProvider implements AuthenticationProvider {
private MyUserDetailsService myUserDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
// 校验验证码
additionalAuthenticationChecks((SmsCodeAuthenticationToken) authentication);
// 校验手机号
String mobile = authentication.getPrincipal().toString();
UserDetails userDetails = myUserDetailsService.loadUserByMobile(mobile);
if (null == userDetails) {
throw new BadCredentialsException("手机号不存在");
}
// 创建认证成功的Authentication对象
SmsCodeAuthenticationToken result = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
result.setDetails(authentication.getDetails());
return result;
}
protected void additionalAuthenticationChecks(SmsCodeAuthenticationToken authentication) throws AuthenticationException {
if (authentication.getCredentials() == null) {
throw new BadCredentialsException("验证码不能为空");
}
String mobile = authentication.getPrincipal().toString();
String smsCode = authentication.getCredentials().toString();
// 从Session或者Redis中获取相应的验证码
String smsCodeInSessionKey = "SMS_CODE_" + mobile;
// String verificationCode = sessionStrategy.getAttribute(servletWebRequest, smsCodeInSessionKey);
// String verificationCode = stringRedisTemplate.opsForValue().get(smsCodeInSessionKey);
String verificationCode = "1234";
if (StringUtils.isBlank(verificationCode)) {
throw new BadCredentialsException("短信验证码不存在,请重新发送!");
}
if (!smsCode.equalsIgnoreCase(verificationCode)) {
throw new BadCredentialsException("验证码错误!");
}
//todo 清除Session或者Redis中获取相应的验证码
}
@Override
public boolean supports(Class<?> authentication) {
return (SmsCodeAuthenticationToken.class.isAssignableFrom(authentication));
}
public MyUserDetailsService getMyUserDetailsService() {
return myUserDetailsService;
}
public void setMyUserDetailsService(MyUserDetailsService myUserDetailsService) {
this.myUserDetailsService = myUserDetailsService;
}
}
参照org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter写一个短信认证处理的过滤器
package com.example.demo.filter;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Author ChengJianSheng
* @Date 2021/5/12
*/
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "smsCode";
private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/login/mobile", "POST");
private String usernameParameter = SPRING_SECURITY_FORM_MOBILE_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
private boolean postOnly = true;
public SmsAuthenticationFilter() {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
}
public SmsAuthenticationFilter(AuthenticationManager authenticationManager) {
super(DEFAULT_ANT_PATH_REQUEST_MATCHER, authenticationManager);
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
}
String mobile = obtainMobile(request);
mobile = (mobile != null) ? mobile : "";
mobile = mobile.trim();
String smsCode = obtainPassword(request);
smsCode = (smsCode != null) ? smsCode : "";
SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, smsCode);
setDetails(request, authRequest);
return this.getAuthenticationManager().authenticate(authRequest);
}
private String obtainMobile(HttpServletRequest request) {
return request.getParameter(this.usernameParameter);
}
private String obtainPassword(HttpServletRequest request) {
return request.getParameter(this.passwordParameter);
}
protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
}
在WebSecurity中进行配置
package com.example.demo.config;
import com.example.demo.filter.SmsAuthenticationFilter;
import com.example.demo.filter.SmsAuthenticationProvider;
import com.example.demo.handler.MyAuthenticationFailureHandler;
import com.example.demo.handler.MyAuthenticationSuccessHandler;
import com.example.demo.service.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.SecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.DefaultSecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
/**
* @Author ChengJianSheng
* @Date 2021/5/12
*/
@Component
public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
@Autowired
private MyUserDetailsService myUserDetailsService;
@Autowired
private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;
@Autowired
private MyAuthenticationFailureHandler myAuthenticationFailureHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
smsAuthenticationFilter.setAuthenticationSuccessHandler(myAuthenticationSuccessHandler);
smsAuthenticationFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);
SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
smsAuthenticationProvider.setMyUserDetailsService(myUserDetailsService);
http.authenticationProvider(smsAuthenticationProvider)
.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
}
}
http.apply(smsAuthenticationConfig);