# spring-security **Repository Path**: liang-tian-yu/spring-security ## Basic Information - **Project Name**: spring-security - **Description**: spring-security权限管理 - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2025-06-23 - **Last Updated**: 2025-07-07 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README ## SpringSecurity [参考文章](https://juejin.cn/post/7400195628847382564) 说明:使用SpringSecurity + jjwt 实现 - 导入依赖 ``` org.springframework.boot spring-boot-starter-security io.jsonwebtoken jjwt 0.9.1 ``` - application配置 ``` jwt: # token过期时间,1个小时 expiration: 3600000 # token在header中的属性名 tokenHeader: Authorization # 生成token的密钥 secret: jwt-token-secret ``` ### 代码实现 - JwtTokenUtil(创建`jwt`工具类,方便实现根据用户信息生成`token`,以及通过`token`中获取用户信息) ``` package com.lty.security; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Clock; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.impl.DefaultClock; import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.stereotype.Component; import java.io.Serializable; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.function.Function; /** * jwt工具类 */ @Component @Data public class JwtTokenUtil implements Serializable { private static final long serialVersionUID = -3301605591108950415L; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; private Clock clock = DefaultClock.INSTANCE; // 根据用户信息生成token public String generateToken(UserDetails userDetails) { Map claims = new HashMap<>(); return doGenerateToken(claims, userDetails.getUsername()); } private String doGenerateToken(Map claims, String subject) { final Date createdDate = clock.now(); final Date expirationDate = calculateExpirationDate(createdDate); return Jwts.builder() .setClaims(claims) .setSubject(subject) .setIssuedAt(createdDate) .setExpiration(expirationDate) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } private Date calculateExpirationDate(Date createdDate) { return new Date(createdDate.getTime() + expiration); } public Boolean validateToken(String token, UserDetails userDetails) { SecurityUserDetails user = (SecurityUserDetails) userDetails; final String username = getUsernameFromToken(token); return (username.equals(user.getUsername()) && !isTokenExpired(token) ); } // 通过token获取用户名username public String getUsernameFromToken(String token) { return getClaimFromToken(token, Claims::getSubject); } public T getClaimFromToken(String token, Function claimsResolver) { final Claims claims = getAllClaimsFromToken(token); return claimsResolver.apply(claims); } private Claims getAllClaimsFromToken(String token) { return Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } private Boolean isTokenExpired(String token) { final Date expiration = getExpirationDateFromToken(token); return expiration.before(clock.now()); } public Date getExpirationDateFromToken(String token) { return getClaimFromToken(token, Claims::getExpiration); } } ``` - 用户信息 ``` package com.lty.security.model; import lombok.Data; @Data public class SysUser { private Integer id; private String username; private String password; } ``` ``` package com.lty.security; import com.lty.security.model.SysUser; import lombok.Data; import lombok.EqualsAndHashCode; import lombok.experimental.Accessors; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import java.util.Collection; /** * 自定义用户信息 */ @Data @EqualsAndHashCode @Accessors(chain = true) // 实现链式set方法 public class SecurityUserDetails extends SysUser implements UserDetails { // 权限列表 private Collection authorities; public SecurityUserDetails(String userName, Collection authorities) { // TODO 这里可以从数据库中查询用户信息 this.setUsername(userName); String encode = new BCryptPasswordEncoder().encode("123456"); this.setPassword(encode); this.setAuthorities(authorities); } /** * 下面这些都返回true * * @return */ @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return true; } } ``` - 重写`UserDetailsService`的`loadUserByUsername`方法实现具体的认证授权逻辑 ``` package com.lty.security; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; 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 java.util.ArrayList; import java.util.List; @Service public class JwtUserDetailsServiceImpl implements UserDetailsService { /** * 实现具体的认证授权逻辑 * @param username * @return * @throws UsernameNotFoundException */ @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { List authorityList = new ArrayList<>(); // TODO 这里直接把用户的权限写死,ROLE_USER表示用户拥有USER权限,因为权限都是以ROLE_开头的。 authorityList.add(new SimpleGrantedAuthority("ROLE_USER")); return new SecurityUserDetails(username,authorityList); } } ``` - 建一个用户请求的过滤器,用来拦截用户请求,分析用户有没有该请求的权限 ``` package com.lty.security; import io.jsonwebtoken.ExpiredJwtException; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; 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; /** * 拦截用户请求(拦截用户请求,分析用户有没有该请求的权限) */ @Component public class JwtAuthorizationTokenFilter extends OncePerRequestFilter { private final UserDetailsService userDetailsService; private final JwtTokenUtil jwtTokenUtil; private final String tokenHeader; public JwtAuthorizationTokenFilter(@Qualifier("jwtUserDetailsServiceImpl") UserDetailsService userDetailsService, JwtTokenUtil jwtTokenUtil, @Value("${jwt.tokenHeader}") String tokenHeader) { this.userDetailsService = userDetailsService; this.jwtTokenUtil = jwtTokenUtil; this.tokenHeader = tokenHeader; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { final String requestHeader = request.getHeader(this.tokenHeader); String username = null; String authToken = null; if (requestHeader != null && requestHeader.startsWith("Bearer ")) { authToken = requestHeader.substring(7); try { username = jwtTokenUtil.getUsernameFromToken(authToken); } catch (ExpiredJwtException e) { e.printStackTrace(); } } // 验证用户身份 if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request, response); } } ``` - 实现`AuthenticationEntryPoint`接口的`commence`方法,当请求没有携带认证信息或者说认证失败时,使用我们自己编写的处理逻辑 ``` package com.lty.security; 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; /** * 认证失败处理类(当请求访问需要认证,但认证失败时,会调用此方法) */ @Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { // TODO 自定义的错误逻辑 response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.getWriter().write("{\"code\":401,\"message\":\"未授权,请登录\"}"); // 返回JSON格式的错误信息 } } ``` - `Security`的核心配置类,重写`WebSecurityConfigurerAdapter`中的`configure`方法 ``` package com.lty.security; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Lazy; import org.springframework.http.HttpMethod; import org.springframework.security.authentication.AuthenticationManager; 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.builders.WebSecurity; 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; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint; @Autowired JwtUserDetailsServiceImpl jwtUserDetailsService; @Autowired JwtAuthorizationTokenFilter authenticationTokenFilter; @Autowired @Lazy PasswordEncoder passwordEncoder; @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } @Override protected void configure(HttpSecurity http) throws Exception { http .exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint) // 使用自定义的认证失败方法 .and() .authorizeRequests() .antMatchers("/login").permitAll() .antMatchers(HttpMethod.OPTIONS, "/**").anonymous() .anyRequest().authenticated() // 除上面以外的都拦截 .and() .csrf().disable() // 禁用security自带的跨域处理 // 让Security不使用session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); http.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); } @Override public void configure(WebSecurity web) { // 配置静态文件不需要认证 web.ignoring().antMatchers("/css/**"); web.ignoring().antMatchers("/js/**"); } /** * 指定加密方式 */ @Bean public PasswordEncoder passwordEncoderBean() { return new BCryptPasswordEncoder(); } /** * 认证逻辑配置 */ @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder); } } ``` ### 测试 ``` package com.lty.controller; import com.lty.security.JwtTokenUtil; import com.lty.security.model.SysUser; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpServletRequest; @RestController public class LoginController { @Autowired @Qualifier("jwtUserDetailsServiceImpl") private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @PostMapping("/login") public String login(@RequestBody SysUser sysUser, HttpServletRequest request){ final UserDetails userDetails = userDetailsService.loadUserByUsername(sysUser.getUsername()); final String token = jwtTokenUtil.generateToken(userDetails); return token; } @PreAuthorize("hasAnyRole('USER')") @PostMapping(value = "/testUser") public String testNeed() { // 测试用户权限 return "hello world"; } @PostMapping(value = "/info") public SysUser info() { // 获取当前登录用户信息 Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); SysUser user = null; if (authentication != null && authentication.isAuthenticated()) { user = ((SysUser) authentication.getPrincipal()); } return user; } } ``` - 接口测试 登陆接口 ``` curl --location --request POST 'http://localhost:8088/api/login' \ --header 'Content-Type: application/json' \ --data-raw '{ "username":"test" }' ``` 权限认证 ``` curl --location --request POST 'http://localhost:8088/api/testUser' \ --header 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzUwNjUwNzQzLCJpYXQiOjE3NTA2NDcxNDN9.e3FXSx2MVPe7CEUmLjWWqb-3VXntO5VrvTsCsNACa7NAQOQ8xqidJWDUhsf0vzQW2oZO0CB8RE_Jvqfjzalj4g' \ --header 'Cookie: JSESSIONID=81F5FE1594C0C236FCDE21E866C6BA6B' ``` 用户信息 ``` curl --location --request POST 'http://localhost:8088/api/info' \ --header 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ0ZXN0IiwiZXhwIjoxNzUwNjUwNzQzLCJpYXQiOjE3NTA2NDcxNDN9.e3FXSx2MVPe7CEUmLjWWqb-3VXntO5VrvTsCsNACa7NAQOQ8xqidJWDUhsf0vzQW2oZO0CB8RE_Jvqfjzalj4g' \ --header 'Cookie: JSESSIONID=81F5FE1594C0C236FCDE21E866C6BA6B; JSESSIONID=81F5FE1594C0C236FCDE21E866C6BA6B' ```