快速创建应用
采用Springboot、Maven、jdk8,快速创建一个Web应用。
基础访问类编写
package com.example.demo.controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
*
* @author 小隐乐乐
* @since 2020/11/8 19:44
*/
@RestController
@RequestMapping("/api")
public class HelloController {
@GetMapping("/hello")
public String hello() {
return "hello guys";
}
}
配置文件修改
application.properties
添加端口信息
server.port=8000
POM
<?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.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
测试应用
启动应用,访问接口 : localhost:8000/api/hello
应用创建成功。
添加Spring Boot Security支持
POM
<?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.3.5.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<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>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
测试应用
启动应用,日志如下:
Springboot Security会创建一个用户,还有一个请求安全链。用户默认用户名user 密码,随机生成,打印在启动日志中,此时为abfb4748-61e9-45d8-bc22-72a21d45df6a
测试应用,访问接口 : localhost:8000/api/hello
页面自动跳转到登录页面
输入用户名、密码,之后会跳转到接口响应页面
用户名密码修改
可以在配置文件中自定义,用户名/密码
application.properties
server.port=8000
spring.security.user.name=levi
spring.security.user.password=123456
MVC Security
默认的安全配置在SecurityAutoConfiguration和UserDetailsServiceAutoConfiguration中实现。 SecurityAutoConfiguration实现SpringBootWebSecurityConfigurationWeb安全性并通过UserDetailsServiceAutoConfiguration配置身份验证,这在非Web应用程序中也很重要。要完全关闭默认的Web应用程序安全性配置或合并多个Spring Security组件(例如OAuth 2 Client和Resource Server),请添加一个类型的bean WebSecurityConfigurerAdapter(这样做不会禁用UserDetailsService配置或Actuator的安全性)。
为了关闭UserDetailsService的配置,可以添加类型的UserDetailsService,AuthenticationProvider或AuthenticationManager。
可以通过添加自定义来覆盖访问规则WebSecurityConfigurerAdapter。Spring Boot提供了便利的方法,可用于覆盖执行器端点和静态资源的访问规则。 EndpointRequest可用于创建RequestMatcher基于management.endpoints.web.base-path属性的。 PathRequest可用于RequestMatcher在常用位置创建for资源。
详细可以参考官方文档介绍 docs.spring.io/spring-boot…
开发配置类
用于自定义拦截配置
package com.example.demo.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
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.provisioning.InMemoryUserDetailsManager;
/**
* @author 小隐乐乐
* @since 2020/11/8 20:20
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/signin").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
@Override
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
开发登录实体类
用于完成登录实体
package com.example.demo.dto;
import com.sun.istack.internal.NotNull;
/**
* @author 小隐乐乐
* @since 2020/11/8 20:22
*/
public class SigninDto {
@NotNull
private String username;
@NotNull
private String password;
protected SigninDto() {}
public SigninDto(String username, String password) {
this.username = username;
this.password = password;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
改造controller
package com.example.demo.controller;
import com.example.demo.dto.SigninDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
*
* @author 小隐乐乐
* @since 2020/11/8 19:44
*/
@RestController
@RequestMapping("/api")
public class HelloController {
@Autowired
private AuthenticationManager authenticationManager;
@GetMapping("/hello")
public String hello() {
return "hello guys";
}
@PostMapping("/signin")
public Authentication signIn(@RequestBody @Valid SigninDto signInDto) {
return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(signInDto.getUsername(), signInDto.getPassword()));
}
}
目录结构
此时完成了添加端点signin到自定义拦截链中
测试应用
此时接口api/hello,仍然是没有权限的,我们添加jwt的支持。
添加JWT
依赖添加
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
开发token生成类
package com.example.demo.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Base64;
import java.util.Date;
/**
* @author 小隐乐乐
* @since 2020/11/8 20:38
*/
@Component
public class JwtProvider {
private String secretKey;
private long validityInMilliseconds;
@Autowired
public JwtProvider(@Value("${security.jwt.token.secret-key}") String secretKey,
@Value("${security.jwt.token.expiration}") long milliseconds) {
this.secretKey = Base64.getEncoder().encodeToString(secretKey.getBytes());
this.validityInMilliseconds = milliseconds;
}
public String createToken(String username) {
//Add the username to the payload
Claims claims = Jwts.claims().setSubject(username);
//Build the Token
Date now = new Date();
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + validityInMilliseconds))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
}
create Token方法使用jjwt库创建JWT令牌。用户名就是有效负载的主题。它是使用属性文件中的密钥签名的,并且令牌的有效性也在属性文件中指定。结果令牌将具有Header.Payload.Signature的格式。标头包含类型(JSON Web令牌)和哈希算法(HMAC SHA256)。有效负载又称包含令牌的主题(子),数字日期值(exp)中的到期日期,发布JWT的时间(iat),唯一的JWT标识符(jti)以及由冒号分隔的特定于应用程序的键值对。签名是使用嵌入在应用程序中的密钥的标头和有效负载的哈希值。
配置文件修改
security.jwt.token.secret-key=jwt-token-secret-key-for-encryption
security.jwt.token.expiration=1200000
开发web filter
package com.example.demo.jwt;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
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.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.GenericFilterBean;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.util.Base64;
import java.util.Date;
import java.util.Optional;
import java.util.function.Function;
/**
* @author 小隐乐乐
* @since 2020/11/8 20:46
*/
public class JwtTokenFilter extends GenericFilterBean {
private String secret;
private static final String BEARER = "Bearer";
private UserDetailsService userDetailsService;
public JwtTokenFilter(UserDetailsService userDetailsService, String secret) {
this.userDetailsService = userDetailsService;
this.secret = Base64.getEncoder().encodeToString(secret.getBytes());
}
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain filterChain)
throws IOException, ServletException {
String headerValue = ((HttpServletRequest) req).getHeader("Authorization");
getBearerToken(headerValue).ifPresent(token -> {
String username = getClaimFromTokenS((String) token, Claims::getSubject);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (username.equals(userDetails.getUsername()) && !isJwtExpired((String) token)) {
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails((HttpServletRequest) req));
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
}
});
filterChain.doFilter(req, res);
}
private Optional getBearerToken(String headerVal) {
if (headerVal != null && headerVal.startsWith(BEARER)) {
return Optional.of(headerVal.replace(BEARER, "").trim());
}
return Optional.empty();
}
private Date getClaimFromToken(String token, Function<Claims, Date> claimsResolver) {
final Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return claimsResolver.apply(claims);
}
private String getClaimFromTokenS(String token, Function<Claims, String> claimsResolver) {
final Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
return claimsResolver.apply(claims);
}
private boolean isJwtExpired(String token) {
Date expirationDate = getClaimFromToken(token, Claims::getExpiration);
return expirationDate.before(new Date());
}
}
这与Java EE中的Web过滤器相同。此Web筛选器检查令牌是否已过期(即Jwt已过期)。它解析有效负载,获取来自令牌的Claims。它将剥离承载字符串,该字符串是授权标头的值。如果令牌通过了所有检查,则do Filter方法将配置Spring安全性以在上下文中手动设置身份验证。我们指定当前用户已通过身份验证,以使其成功通过。
接下来是在Web安全配置类中添加一些行。
WebSecurityConfiguration改造
package com.example.demo.config;
import com.example.demo.jwt.JwtTokenFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
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.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/**
* @author 小隐乐乐
* @since 2020/11/8 20:20
*/
@Configuration
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Value("${security.jwt.token.secret-key}")
private String secret;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/api/signin").permitAll()
.anyRequest().authenticated()
.and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(new JwtTokenFilter(userDetailsService, secret), UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
@Override
public UserDetailsService userDetailsService() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
接口类改造
package com.example.demo.controller;
import com.example.demo.dto.SigninDto;
import com.example.demo.jwt.JwtProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.AuthenticationException;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
/**
* @author 小隐乐乐
* @since 2020/11/8 19:44
*/
@RestController
@RequestMapping("/api")
public class HelloController {
@Autowired
private JwtProvider jwtProvider;
@Autowired
private AuthenticationManager authenticationManager;
@GetMapping("/hello")
public String hello() {
return "hello guys";
}
@PostMapping("/signin")
public String signIn(@RequestBody @Valid SigninDto signInDto) {
try {
authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(signInDto.getUsername(), signInDto.getPassword()));
return jwtProvider.createToken(signInDto.getUsername());
} catch (AuthenticationException e) {
System.out.println("Log in failed for user, " + signInDto.getUsername());
}
return "";
}
}
测试应用
signin接口调用获取token
hello接口调用携带token
总结
Spring Boot Security和JWT Hello World示例已完成。总而言之,当登录身份验证通过时,我们将创建一个JSON Web令牌并将其返回给调用方。然后,调用方将JWT放置在标头中,并在其后续的GET请求中使用授权密钥。 Web过滤器检查令牌的有效性。如果有效,Web过滤器将使其通过过滤器链并返回“ Hello world”。