SpringBoot Security
是Spring官方提供的一个安全框架,他的核心功能是对系统用户进行认证和鉴权,也经常在项目中被使用到,本文不介绍其太过深入的内容,只介绍如何实现并完成认证和鉴权的测试。主要分三步来实现:
- 配置JWT
- 配置Security
- 编写测试相关代码
首先创建一个springboot项目,我的版本是2.6.13
,依然是java8
,整合Security+JWT需要用到的Maven
依赖如下:
xml
代码解读
复制代码
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--jwt--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency>
配置JWT
- 先在
yml
配置文件中添加jwt
相关配置yml
代码解读
复制代码
jwt: expiration: 3600000 //token过期时间,1个小时 tokenHeader: Authorization //token在header中的属性名 secret: jwt-token-secret //生成token的密钥
- 创建
jwt
工具类,方便实现根据用户信息生成token
,以及通过token
中获取用户信息java
代码解读
复制代码
@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<String, Object> claims = new HashMap<>(); return doGenerateToken(claims, userDetails.getUsername()); } private String doGenerateToken(Map<String, Object> 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> T getClaimFromToken(String token, Function<Claims, T> 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); } }
配置Security
-
编写一个存储用户信息的
UserDetails
的实现类java
代码解读
复制代码
@Data public class SysUser { private Integer id; private String username; private String password; }
整理了一份Java面试题。包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题
需要全套面试笔记的【点击此处】即可免费获取
java
代码解读
复制代码
@Data @EqualsAndHashCode @Accessors(chain = true) //实现链式set方法 public class SecurityUserDetails extends SysUser implements UserDetails { //权限列表 private Collection<? extends GrantedAuthority> authorities; public SecurityUserDetails(String userName,Collection<? extends GrantedAuthority> authorities){ 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; } }
提示
因为只是记录一下如何实现security+jwt,所以没有从数据库中读取真实的用户信息,而是直接将用户信息和权限信息写死测试。 -
重写
UserDetailsService
的loadUserByUsername
方法实现具体的认证授权逻辑java
代码解读
复制代码
@Service public class JwtUserDetailsServiceImpl implements UserDetailsService { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { List<GrantedAuthority> authorityList = new ArrayList<>(); authorityList.add(new SimpleGrantedAuthority("ROLE_USER")); return new SecurityUserDetails(username,authorityList); } }
提示
这里直接把用户的权限写死,ROLE_USER表示用户拥有USER
权限,因为权限都是以ROLE_
开头的。 -
紧接着创建一个用户请求的过滤器,用来拦截用户请求,分析用户有没有该请求的权限
java
代码解读
复制代码
@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); } }
提示
Bearer
必须带空格,第二个if
判断就是为了加载到用户的信息,并且在Security
上下文中存储用户及用户的权限的信息 -
实现
AuthenticationEntryPoint
接口的commence
方法,当请求没有携带认证信息或者说认证失败时,使用我们自己编写的处理逻辑。java
代码解读
复制代码
@Component public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.sendError(HttpServletResponse.SC_UNAUTHORIZED); } }
提示
如果请求没有携带认证信息或者说认证失败时,会返回给客户端401
,如果不重写commence
方法,默认返回403 -
接下来编写
Security
的核心配置类,重写WebSecurityConfigurerAdapter
中的configure
方法java
代码解读
复制代码
@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); } @Bean public PasswordEncoder passwordEncoderBean() { return new BCryptPasswordEncoder(); } /** * 认证逻辑配置 */ @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(jwtUserDetailsService).passwordEncoder(passwordEncoder); } }
提示
上面的代码中,.exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint)
表示使用自定义的认证失败处理逻辑。并且配置类中,自定义了用户密码的加密方式,configureGlobal
方法设置自定义的loadUserByUsername
方法实现和校验密码校验的加密方式。
编写测试相关代码
- 编写一个不需要认证授权就能访问的登录接口
/login
java
代码解读
复制代码
@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; } }
- 编写一个需要
USER
权限的接口/sys/testUser
less
代码解读
复制代码
@RestController @RequestMapping("/sys") public class SysUserController { @PreAuthorize("hasAnyRole('USER')") @PostMapping(value = "/testUser") public String testNeed() { return "hello world"; } }
测试
启动SpringBoot项目,对上面的接口进行测试,首先调用/login
接口登录并获取token
请求成功并获取到jwt
生成的token
。紧接着调用需要USER
权限的/testUser
,请求时要在请求头里面携带token
请求成功!
现在来测试一下失败的情况,不传token
直接请求
请求失败,返回401
,表示没有认证。再来测试一下如果将@PreAuthorize("hasAnyRole('USER')")
中的权限改为Admin
,然后用刚刚生成的token
去请求
java
代码解读
复制代码
@PreAuthorize("hasAnyRole('Admin')") @PostMapping(value = "/testUser") public String testNeed() { return "hello world"; }
由于token中包含的授权信息是USER
,所以将@PreAuthorize("hasAnyRole('USER')")
中的USER
改为Admin
后,返回了403
,表示没有这个权限。