基于Spring Security前后端分离的权限控制系统问题

网友投稿 195 2022-10-18


基于Spring Security前后端分离的权限控制系统问题

目录1. 引入maven依赖2. 建表并生成相应的实体类3. 自定义UserDetails4. 自定义各种Handler5. Token处理6. 访问控制7. 配置WebSecurity8. 看效果9. 补充:手机号+短信验证码登录

前后端分离的项目,前端有菜单(menu),后端有API(backendApi),一个menu对应的页面有N个API接口来支持,本文介绍如何基于Spring Security前后端分离的权限控制系统问题。

话不多说,入正题。一个简单的权限控制系统需要考虑的问题如下:

权限如何加载

权限匹配规则

登录

1. 引入maven依赖

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

4.0.0

org.springframework.boot

spring-boot-starter-parent

2.5.1

com.example

demo5

0.0.1-SNAPSHOT

demo5

1.8

org.springframework.boot

spring-boot-starter-data-jpa

org.springframework.boot

spring-boot-starter-data-redis

org.springframework.boot

spring-boot-starter-security

org.springframework.boot

spring-boot-starter-web

io.jsonwebtoken

jjwt

0.9.1

com.alibaba

fastjson

1.2.76

org.apache.commons

commons-lang3

3.12.0

commons-codec

commons-codec

1.15

mysql

mysql-connector-java

runtime

org.projectlombok

lombok

true

org.springframework.boot

spring-boot-maven-plugin

org.projectlombok

lombok

xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">

4.0.0

org.springframework.boot

spring-boot-starter-parent

2.5.1

com.example

demo5

0.0.1-SNAPSHOT

demo5

1.8

org.springframework.boot

spring-boot-starter-data-jpa

org.springframework.boot

spring-boot-starter-data-redis

org.springframework.boot

spring-boot-starter-security

org.springframework.boot

spring-boot-starter-web

io.jsonwebtoken

jjwt

0.9.1

com.alibaba

fastjson

1.2.76

org.apache.commons

commons-lang3

3.12.0

commons-codec

commons-codec

1.15

mysql

mysql-connector-java

runtime

org.projectlombok

lombok

true

org.springframework.boot

spring-boot-maven-plugin

org.projectlombok

lombok

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 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 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 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 users;

@ManyToMany

@JoinTable(name = "sys_role_menu",

joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},

inverseJoinColumns = {@JoinColumn(name = "menu_id", referencedColumnName = "id")})

private Set menus;

// @ManyToMany

// @JoinTable(name = "sys_dept_role",

// joinColumns = {@JoinColumn(name = "role_id", referencedColumnName = "id")},

// inverseJoinColumns = {@JoinColumn(name =http:// "dept_id", referencedColumnName = "id")})

// private Set 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 authorities;

public MyUserDetails(String username, String password, boolean enabled, Set 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 roleSet = sysUserEntity.getRoles();

Set 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 roleSet = sysUserEntity.getRoles();

Set 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 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 extractClaim(String token, Function 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 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 {

@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);

以上就是基于 Spring Security前后端分离的权限控制系统的详细内容,更多关于Spring Security权限控制系统的资料请关注我们其它相关文章!


版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。

上一篇:VMware vSphere 6简单部署---VCSA简单使用
下一篇:WAF防火墙接口问题导致业务中断
相关文章

 发表评论

暂时没有评论,来抢沙发吧~