基於Spring Security前後端分離的權限控制系統問題
前後端分離的項目,前端有菜單(menu),後端有API(backendApi),一個menu對應的頁面有N個API接口來支持,本文介紹如何基於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);
以上就是基於 Spring Security前後端分離的權限控制系統的詳細內容,更多關於Spring Security權限控制系統的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- springboot+jwt+springSecurity微信小程序授權登錄問題
- Spring security 自定義過濾器實現Json參數傳遞並兼容表單參數(實例代碼)
- 一文詳解Spring Security的基本用法
- SpringBoot+SpringSecurity+JWT實現系統認證與授權示例
- 使用SpringSecurity 進行自定義Token校驗