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

目录

  • 1. 引入maven依赖
  • 2. 建表并生成相应的实体类
  • 3. 自定义UserDetails
  • 4. 自定义各种Handler
  • 5. Token处理
  • 6. 访问控制
  • 7. 配置WebSecurity
  • 8. 看效果
  • 9. 补充:手机号+短信验证码登录
前后端分离的项目,前端有菜单(menu),后端有API(backendApi),一个menu对应的页面有N个API接口来支持,本文介绍如何基于Spring Security前后端分离的权限控制系统问题。
话不多说,入正题。一个简单的权限控制系统需要考虑的问题如下:
  1. 权限如何加载
  2. 权限匹配规则
  3. 登录

1. 引入maven依赖
4.0.0org.springframework.bootspring-boot-starter-parent2.5.1 com.exampledemo50.0.1-SNAPSHOTdemo5 1.8 org.springframework.bootspring-boot-starter-data-jpaorg.springframework.bootspring-boot-starter-data-redisorg.springframework.bootspring-boot-starter-securityorg.springframework.bootspring-boot-starter-web io.jsonwebtokenjjwt0.9.1 com.alibabafastjson1.2.76org.apache.commonscommons-lang33.12.0commons-codeccommons-codec1.15 mysqlmysql-connector-javaruntimeorg.projectlomboklomboktrue org.springframework.bootspring-boot-maven-pluginorg.projectlomboklombok

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. 建表并生成相应的实体类 基于Spring|基于Spring Security前后端分离的权限控制系统问题
文章图片

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 = "dept_id", referencedColumnName = "id")}) //private Set depts; }

注意,不要使用@Data注解,因为@Data包含@ToString注解
不要随便打印SysUser,例如:System.out.println(sysUser); 任何形式的toString()调用都不要有,否则很有可能造成循环调用,死递归。想想看,SysUser里面要查SysRole,SysRole要查SysMenu,SysMenu又要查SysRole。除非不用懒加载。
基于Spring|基于Spring Security前后端分离的权限控制系统问题
文章图片


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 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; } @Overridepublic Collection getAuthorities() {return authorities; } @Overridepublic String getPassword() {return password; } @Overridepublic String getUsername() {return username; } @Overridepublic boolean isAccountNonExpired() {return true; } @Overridepublic boolean isAccountNonLocked() {return true; } @Overridepublic boolean isCredentialsNonExpired() {return true; } @Overridepublic 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 {@Resourceprivate SysUserRepository sysUserRepository; @Overridepublic 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 {@Resourceprivate SysUserRepository sysUserRepository; @Overridepublic 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(); @Autowiredprivate StringRedisTemplate stringRedisTemplate; @Overridepublic 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(); @Overridepublic 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(); @Overridepublic 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(); @Overridepublic 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(); @Overridepublic 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(); @Autowiredprivate StringRedisTemplate stringRedisTemplate; @Overridepublic 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 { @Autowiredprivate StringRedisTemplate stringRedisTemplate; @Overrideprotected 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 = https://www.it610.com/article/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 { @Autowiredprivate MyUserDetailsService myUserDetailsService; @Autowiredprivate MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowiredprivate MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Autowiredprivate TokenFilter tokenFilter; @Overrideprotected void configure(AuthenticationManagerBuilder auth) throws Exception {auth.userDetailsService(myUserDetailsService).passwordEncoder(passwordEncoder()); } @Overrideprotected 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. 看效果 基于Spring|基于Spring Security前后端分离的权限控制系统问题
文章图片

基于Spring|基于Spring Security前后端分离的权限控制系统问题
文章图片

基于Spring|基于Spring Security前后端分离的权限控制系统问题
文章图片

基于Spring|基于Spring Security前后端分离的权限控制系统问题
文章图片

基于Spring|基于Spring Security前后端分离的权限控制系统问题
文章图片

基于Spring|基于Spring Security前后端分离的权限控制系统问题
文章图片


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 authorities) {super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } @Overridepublic Object getCredentials() {return credentials; } @Overridepublic Object getPrincipal() {return principal; } @Overridepublic void setAuthenticated(boolean authenticated) {Assert.isTrue(!authenticated, "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); super.setAuthenticated(false); } @Overridepublic 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; @Overridepublic 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中获取相应的验证码} @Overridepublic 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); } @Overridepublic 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 { @Autowiredprivate MyUserDetailsService myUserDetailsService; @Autowiredprivate MyAuthenticationSuccessHandler myAuthenticationSuccessHandler; @Autowiredprivate MyAuthenticationFailureHandler myAuthenticationFailureHandler; @Overridepublic 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|基于Spring Security前后端分离的权限控制系统问题】以上就是基于 Spring Security前后端分离的权限控制系统的详细内容,更多关于Spring Security权限控制系统的资料请关注脚本之家其它相关文章!

    推荐阅读