Spring Boot Security Login example with JWT and H2 Database - BezKoderhttps://www.bezkoder.com/spring-boot-security-login-jwt/
在本教程中,我们将构建一个 Spring Boot,Spring Security:登录和注册示例(Rest API),它支持 JWT 和 HttpOnly Cookie 与 H2 数据库一起使用。你会知道:
我们将使用 JWT 构建一个 Spring Boot + Spring Security 应用程序:
这些是我们需要提供的 API:
方法 | 网址 | 行动 |
---|---|---|
POST | /api/auth/signup | 注册新帐户 |
POST | /api/auth/signin | 登录帐户 |
POST | /api/auth/signout | 注销帐户 |
GET | /api/test/all | 检索公共内容 |
GET | /api/test/user | 访问用户的内容 |
GET | /api/test/mod | 访问版主的内容 |
GET | /api/test/admin | 访问管理员的内容 |
通过配置项目依赖和数据源,我们将使用的数据库是 H2。
该图显示了我们如何实现用户注册、用户登录/注销和授权过程的流程。
合法的 JWT 将存储在HttpOnly Cookie中如果客户端访问受保护的资源
您可能需要实现刷新令牌:
更多细节在:Spring Boot Refresh Token with JWT example
您可以通过下图大致了解我们的 Spring Boot 安全登录示例:
现在我将简要解释一下。
春季安全
–WebSecurityConfigurerAdapter
是我们安全实施的关键。它提供HttpSecurity
配置来配置 cors、csrf、会话管理、受保护资源的规则。我们还可以扩展和自定义包含以下元素的默认配置。
–UserDetailsService
接口有一个方法通过用户名加载用户并返回一个UserDetails
Spring Security 可以用于身份验证和验证的对象。
–UserDetails
包含构建身份验证对象所需的信息(例如:用户名、密码、权限)。
–UsernamePasswordAuthenticationToken
从登录请求中获取 {username, password},AuthenticationManager
将使用它来验证登录帐户。
–AuthenticationManager
有一个(在&DaoAuthenticationProvider
的帮助下)来验证对象。如果成功,则返回一个完全填充的 Authentication 对象(包括授予的权限)。UserDetailsService
PasswordEncoder
UsernamePasswordAuthenticationToken
AuthenticationManager
–OncePerRequestFilter
对我们的 API 的每个请求进行一次执行。它提供了一个doFilterInternal()
方法,我们将实现解析和验证 JWT,加载用户详细信息(使用UserDetailsService
),检查授权(使用UsernamePasswordAuthenticationToken
)。
–AuthenticationEntryPoint
将捕获身份验证错误。
Repository包含UserRepository
&RoleRepository
与 Database 一起使用,将被导入Controller。
控制器在被过滤后接收并处理请求OncePerRequestFilter
。
–AuthController
处理注册/登录请求
–TestController
使用基于角色的验证访问受保护的资源方法。
深入了解架构,更容易掌握概览:
Spring Boot Architecture for JWT with Spring Security
这是我们的 Spring Boot 安全登录示例的文件夹和文件结构:
安全性:我们在这里配置 Spring Security 并实现安全对象。
WebSecurityConfig
延伸 WebSecurityConfigurerAdapter
UserDetailsServiceImpl
工具 UserDetailsService
UserDetailsImpl
工具 UserDetails
AuthEntryPointJwt
工具 AuthenticationEntryPoint
AuthTokenFilter
延伸 OncePerRequestFilter
JwtUtils
提供生成、解析、验证 JWT 的方法控制器处理注册/登录请求和授权请求。
AuthController
: @PostMapping('/signup'), @PostMapping('/signin'), @PostMapping('/signout')TestController
: @GetMapping('/api/test/all'), @GetMapping('/api/test/[role]')存储库具有扩展 Spring Data JPAJpaRepository
以与数据库交互的接口。
UserRepository
延伸 JpaRepository
RoleRepository
延伸 JpaRepository
models定义了 Authentication ( User
) 和 Authorization ( Role
) 的两个主要模型。它们具有多对多的关系。
User
: id、用户名、电子邮件、密码、角色Role
:身份证,姓名有效负载定义了请求和响应对象的类
我们还有用于配置 Spring Datasource、Spring Data JPA 和 App 属性(例如 JWT Secret 字符串或 Token 过期时间)的application.properties 。
使用Spring Web 工具或您的开发工具(Spring Tool Suite、Eclipse、Intellij)创建一个 Spring Boot 项目。
然后打开pom.xml并添加这些依赖项:
org.springframework.boot
spring-boot-starter-data-jpa
org.springframework.boot
spring-boot-starter-security
org.springframework.boot
spring-boot-starter-validation
org.springframework.boot
spring-boot-starter-web
io.jsonwebtoken
jjwt
0.9.1
com.h2database
h2
runtime
在src/main/resources文件夹下,打开application.properties,添加一些新行。
spring.h2.console.enabled=true
# default path: h2-console
spring.h2.console.path=/h2-ui
spring.datasource.url=jdbc:h2:file:./testdb
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto= update
# App Properties
bezkoder.app.jwtCookieName= bezkoder
bezkoder.app.jwtSecret= bezKoderSecretKey
bezkoder.app.jwtExpirationMs= 86400000
spring.datasource.url
:jdbc:h2:mem:[database-name]
用于内存数据库和jdbc:h2:file:[path/database-name]
基于磁盘的数据库。spring.datasource.username
&spring.datasource.password
属性与您的数据库安装相同。H2Dialect
为 H2 数据库配置spring.jpa.hibernate.ddl-auto
用于数据库初始化。我们将 value 设置为 value,update
以便在数据库中创建一个表,自动对应于定义的数据模型。对模型的任何更改也将触发对表的更新。对于生产,这个属性应该是validate
。spring.h2.console.enabled=true
告诉 Spring 启动 H2 数据库管理工具,您可以在浏览器上访问此工具:http://localhost:8080/h2-console
.spring.h2.console.path=/h2-ui
用于 H2 控制台的 url,因此默认 urlhttp://localhost:8080/h2-console
将更改为http://localhost:8080/h2-ui
.我们将在数据库中有 3 个表:用户、角色和user_roles用于多对多关系。
让我们定义这些模型。
在模型包中,创建 3 个文件:
ERole
ERole.java中的枚举。
在这个例子中,我们有 3 个角色对应于 3 个枚举。
package com.bezkoder.spring.security.login.models;
public enum ERole {
ROLE_USER,
ROLE_MODERATOR,
ROLE_ADMIN
}
Role
Role.java中的模型
package com.bezkoder.spring.security.login.models;
import javax.persistence.*;
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Enumerated(EnumType.STRING)
@Column(length = 20)
private ERole name;
public Role() {
}
public Role(ERole name) {
this.name = name;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public ERole getName() {
return name;
}
public void setName(ERole name) {
this.name = name;
}
}
User
User.java中的模型。
它有 5 个字段:id、用户名、电子邮件、密码、角色。
package com.bezkoder.spring.security.login.models;
import java.util.HashSet;
import java.util.Set;
import javax.persistence.*;
import javax.validation.constraints.Email;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.Size;
@Entity
@Table(name = "users",
uniqueConstraints = {
@UniqueConstraint(columnNames = "username"),
@UniqueConstraint(columnNames = "email")
})
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@NotBlank
@Size(max = 20)
private String username;
@NotBlank
@Size(max = 50)
@Email
private String email;
@NotBlank
@Size(max = 120)
private String password;
@ManyToMany(fetch = FetchType.LAZY)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set roles = new HashSet<>();
public User() {
}
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Set getRoles() {
return roles;
}
public void setRoles(Set roles) {
this.roles = roles;
}
}
现在,上面的每个模型都需要一个用于持久化和访问数据的存储库。在存储库中包中,让我们创建 2 个存储库。
用户存储库
有 3 种必要的方法JpaRepository
支持。
package com.bezkoder.spring.security.login.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.bezkoder.spring.security.login.models.User;
@Repository
public interface UserRepository extends JpaRepository {
Optional findByUsername(String username);
Boolean existsByUsername(String username);
Boolean existsByEmail(String email);
}
角色存储库
该存储库还扩展JpaRepository
并提供了一个查找器方法。
package com.bezkoder.spring.security.login.repository;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.bezkoder.spring.security.login.models.ERole;
import com.bezkoder.spring.security.login.models.Role;
@Repository
public interface RoleRepository extends JpaRepository {
Optional findByName(ERole name);
}
在安全包中,创建WebSecurityConfig
扩展的类WebSecurityConfigurerAdapter
。
WebSecurityConfig.java
package com.bezkoder.spring.security.login.security;
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.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;
import com.bezkoder.spring.security.login.security.jwt.AuthEntryPointJwt;
import com.bezkoder.spring.security.login.security.jwt.AuthTokenFilter;
import com.bezkoder.spring.security.login.security.services.UserDetailsServiceImpl;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
// securedEnabled = true,
// jsr250Enabled = true,
prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${spring.h2.console.path}")
private String h2ConsolePath;
@Autowired
UserDetailsServiceImpl userDetailsService;
@Autowired
private AuthEntryPointJwt unauthorizedHandler;
@Bean
public AuthTokenFilter authenticationJwtTokenFilter() {
return new AuthTokenFilter();
}
@Override
public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests().antMatchers("/api/auth/**").permitAll()
.antMatchers("/api/test/**").permitAll()
.antMatchers(h2ConsolePath + "/**").permitAll()
.anyRequest().authenticated();
// fix H2 database console: Refused to display ' in a frame because it set 'X-Frame-Options' to 'deny'
http.headers().frameOptions().sameOrigin();
http.addFilterBefore(authenticationJwtTokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
让我解释一下上面的代码。
–@EnableWebSecurity
允许 Spring 查找并自动将类应用于全局 Web 安全。
–@EnableGlobalMethodSecurity
为方法提供 AOP 安全性。它启用@PreAuthorize
,,@PostAuthorize
它还支持JSR-250。您可以在方法安全表达式中的配置中找到更多参数。
– 我们覆盖了接口中的configure(HttpSecurity http)
方法。WebSecurityConfigurerAdapter
它告诉 Spring Security 我们如何配置 CORS 和 CSRF,何时我们想要要求所有用户都经过身份验证,哪个过滤器(AuthTokenFilter
)以及我们希望它何时工作(过滤之前UsernamePasswordAuthenticationFilter
),选择哪个异常处理程序(AuthEntryPointJwt
)。
– Spring Security 将加载用户详细信息以执行身份验证和授权。所以它有UserDetailsService
我们需要实现的接口。
– 的实现UserDetailsService
将用于DaoAuthenticationProvider
按AuthenticationManagerBuilder.userDetailsService()
方法配置。
– 我们还需要一个PasswordEncoder
用于DaoAuthenticationProvider
. 如果我们不指定,它将使用纯文本。
如果身份验证过程成功,我们可以从对象中获取用户的信息,例如用户名、密码、权限Authentication
。
Authentication authentication =
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(username, password)
);
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
// userDetails.getUsername()
// userDetails.getPassword()
// userDetails.getAuthorities()
如果我们想获取更多数据(id、email……),我们可以创建这个UserDetails
接口的实现。
安全/服务/UserDetailsImpl.java
package com.bezkoder.spring.security.login.security.services;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.bezkoder.spring.security.login.models.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
public class UserDetailsImpl implements UserDetails {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String email;
@JsonIgnore
private String password;
private Collection extends GrantedAuthority> authorities;
public UserDetailsImpl(Long id, String username, String email, String password,
Collection extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.email = email;
this.password = password;
this.authorities = authorities;
}
public static UserDetailsImpl build(User user) {
List authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getName().name()))
.collect(Collectors.toList());
return new UserDetailsImpl(
user.getId(),
user.getUsername(),
user.getEmail(),
user.getPassword(),
authorities);
}
@Override
public Collection extends GrantedAuthority> getAuthorities() {
return authorities;
}
public Long getId() {
return id;
}
public String getEmail() {
return email;
}
@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 true;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (o == null || getClass() != o.getClass())
return false;
UserDetailsImpl user = (UserDetailsImpl) o;
return Objects.equals(id, user.id);
}
}
看上面的代码,你可以注意到我们转换Set
成List
. Authentication
稍后使用 Spring Security 和对象很重要。
正如我之前所说,我们需要UserDetailsService
获取UserDetails
对象。您可以查看UserDetailsService
只有一种方法的接口:
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
所以我们实现它并覆盖loadUserByUsername()
方法。
安全/服务/UserDetailsServiceImpl.java
package com.bezkoder.spring.security.login.security.services;
import org.springframework.beans.factory.annotation.Autowired;
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 org.springframework.transaction.annotation.Transactional;
import com.bezkoder.spring.security.login.models.User;
import com.bezkoder.spring.security.login.repository.UserRepository;
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserRepository userRepository;
@Override
@Transactional
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User Not Found with username: " + username));
return UserDetailsImpl.build(user);
}
}
在上面的代码中,我们使用 获取完整的自定义 User 对象,然后我们使用静态方法UserRepository
构建一个对象。UserDetails
build()
让我们定义一个每个请求执行一次的过滤器。所以我们创建AuthTokenFilter
了扩展OncePerRequestFilter
和覆盖doFilterInternal()
方法的类。
安全/jwt/AuthTokenFilter.java
package com.bezkoder.spring.security.login.security.jwt;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import com.bezkoder.spring.security.login.security.services.UserDetailsServiceImpl;
public class AuthTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtUtils jwtUtils;
@Autowired
private UserDetailsServiceImpl userDetailsService;
private static final Logger logger = LoggerFactory.getLogger(AuthTokenFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = parseJwt(request);
if (jwt != null && jwtUtils.validateJwtToken(jwt)) {
String username = jwtUtils.getUserNameFromJwtToken(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails,
null,
userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
} catch (Exception e) {
logger.error("Cannot set user authentication: {}", e);
}
filterChain.doFilter(request, response);
}
private String parseJwt(HttpServletRequest request) {
String jwt = jwtUtils.getJwtFromCookies(request);
return jwt;
}
}
我们在里面做什么doFilterInternal()
:
–JWT
从 HTTP Cookies 中获取
– 如果请求有,验证它,从中JWT
解析– 从,获取创建一个对象–使用方法在SecurityContext中设置当前值。username
username
UserDetails
Authentication
UserDetails
setAuthentication(authentication)
在此之后,每次你想得到UserDetails
,只需SecurityContext
像这样使用:
UserDetails userDetails =
(UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
// userDetails.getUsername()
// userDetails.getPassword()
// userDetails.getAuthorities()
这个类有3个主要功能:
getJwtFromCookies
:JWT
通过 Cookie 名称从 Cookies 中获取generateJwtCookie
: 生成一个包含JWT
用户名、日期、过期时间、秘密的 CookiegetCleanJwtCookie
: 返回带有null
值的 Cookie(用于干净的 Cookie)getUserNameFromJwtToken
: 获取用户名JWT
validateJwtToken
JWT
:用秘密验证 a安全/jwt/JwtUtils.java
package com.bezkoder.spring.security.login.security.jwt;
import java.util.Date;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseCookie;
import org.springframework.stereotype.Component;
import org.springframework.web.util.WebUtils;
import com.bezkoder.spring.security.login.security.services.UserDetailsImpl;
import io.jsonwebtoken.*;
@Component
public class JwtUtils {
private static final Logger logger = LoggerFactory.getLogger(JwtUtils.class);
@Value("${bezkoder.app.jwtSecret}")
private String jwtSecret;
@Value("${bezkoder.app.jwtExpirationMs}")
private int jwtExpirationMs;
@Value("${bezkoder.app.jwtCookieName}")
private String jwtCookie;
public String getJwtFromCookies(HttpServletRequest request) {
Cookie cookie = WebUtils.getCookie(request, jwtCookie);
if (cookie != null) {
return cookie.getValue();
} else {
return null;
}
}
public ResponseCookie generateJwtCookie(UserDetailsImpl userPrincipal) {
String jwt = generateTokenFromUsername(userPrincipal.getUsername());
ResponseCookie cookie = ResponseCookie.from(jwtCookie, jwt).path("/api").maxAge(24 * 60 * 60).httpOnly(true).build();
return cookie;
}
public ResponseCookie getCleanJwtCookie() {
ResponseCookie cookie = ResponseCookie.from(jwtCookie, null).path("/api").build();
return cookie;
}
public String getUserNameFromJwtToken(String token) {
return Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(token).getBody().getSubject();
}
public boolean validateJwtToken(String authToken) {
try {
Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
return true;
} catch (SignatureException e) {
logger.error("Invalid JWT signature: {}", e.getMessage());
} catch (MalformedJwtException e) {
logger.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
logger.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
logger.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
logger.error("JWT claims string is empty: {}", e.getMessage());
}
return false;
}
public String generateTokenFromUsername(String username) {
return Jwts.builder()
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + jwtExpirationMs))
.signWith(SignatureAlgorithm.HS512, jwtSecret)
.compact();
}
}
请记住,我们在文件中添加了bezkoder.app.jwtSecret
,bezkoder.app.jwtExpirationMs
和bezkoder.app.jwtCookieName
属性application.properties
。
现在我们创建AuthEntryPointJwt
实现AuthenticationEntryPoint
接口的类。然后我们重写该commence()
方法。AuthenticationException
每当未经身份验证的用户请求安全的 HTTP 资源并抛出异常时,都会触发此方法。
安全/jwt/AuthEntryPointJwt.java
package com.bezkoder.spring.security.login.security.jwt;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import com.fasterxml.jackson.databind.ObjectMapper;
@Component
public class AuthEntryPointJwt implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(AuthEntryPointJwt.class);
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
logger.error("Unauthorized error: {}", authException.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
final Map body = new HashMap<>();
body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
body.put("error", "Unauthorized");
body.put("message", authException.getMessage());
body.put("path", request.getServletPath());
final ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), body);
}
}
HttpServletResponse.SC_UNAUTHORIZED
是401状态码。表示请求需要 HTTP 认证。
如果要自定义响应数据,只需使用ObjectMapper
类似以下代码:
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
throws IOException, ServletException {
logger.error("Unauthorized error: {}", authException.getMessage());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
final Map body = new HashMap<>();
body.put("status", HttpServletResponse.SC_UNAUTHORIZED);
body.put("error", "Unauthorized");
body.put("message", authException.getMessage());
body.put("path", request.getServletPath());
final ObjectMapper mapper = new ObjectMapper();
mapper.writeValue(response.getOutputStream(), body);
}
我们已经为 Spring Security 构建了所有东西。本教程的下一部分将向您展示如何为我们的 Rest API 实现控制器。
让我总结一下我们的 RestAPI 的有效负载:
– 请求:
– 回应:
为了不让教程太长,我没有在这里展示这些 POJO。您可以在Github
上的项目源代码中找到有效负载类的详细信息。
认证控制器
此控制器提供用于注册和登录、注销操作的 API。
– /api/auth/signup
username
/email
User
(ROLE_USER
如果没有指定角色)User
到数据库UserRepository
– /api/auth/signin
SecurityContext
使用Authentication
对象更新JWT
UserDetails
从Authentication
对象获取JWT
和UserDetails
数据- /api/auth/signout
:清除 Cookie。
控制器/AuthController.java
package com.bezkoder.spring.security.login.controllers;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import javax.validation.Valid;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseCookie;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.bezkoder.spring.security.login.models.ERole;
import com.bezkoder.spring.security.login.models.Role;
import com.bezkoder.spring.security.login.models.User;
import com.bezkoder.spring.security.login.payload.request.LoginRequest;
import com.bezkoder.spring.security.login.payload.request.SignupRequest;
import com.bezkoder.spring.security.login.payload.response.UserInfoResponse;
import com.bezkoder.spring.security.login.payload.response.MessageResponse;
import com.bezkoder.spring.security.login.repository.RoleRepository;
import com.bezkoder.spring.security.login.repository.UserRepository;
import com.bezkoder.spring.security.login.security.jwt.JwtUtils;
import com.bezkoder.spring.security.login.security.services.UserDetailsImpl;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
AuthenticationManager authenticationManager;
@Autowired
UserRepository userRepository;
@Autowired
RoleRepository roleRepository;
@Autowired
PasswordEncoder encoder;
@Autowired
JwtUtils jwtUtils;
@PostMapping("/signin")
public ResponseEntity> authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
ResponseCookie jwtCookie = jwtUtils.generateJwtCookie(userDetails);
List roles = userDetails.getAuthorities().stream()
.map(item -> item.getAuthority())
.collect(Collectors.toList());
return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
.body(new UserInfoResponse(userDetails.getId(),
userDetails.getUsername(),
userDetails.getEmail(),
roles));
}
@PostMapping("/signup")
public ResponseEntity> registerUser(@Valid @RequestBody SignupRequest signUpRequest) {
if (userRepository.existsByUsername(signUpRequest.getUsername())) {
return ResponseEntity.badRequest().body(new MessageResponse("Error: Username is already taken!"));
}
if (userRepository.existsByEmail(signUpRequest.getEmail())) {
return ResponseEntity.badRequest().body(new MessageResponse("Error: Email is already in use!"));
}
// Create new user's account
User user = new User(signUpRequest.getUsername(),
signUpRequest.getEmail(),
encoder.encode(signUpRequest.getPassword()));
Set strRoles = signUpRequest.getRole();
Set roles = new HashSet<>();
if (strRoles == null) {
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
} else {
strRoles.forEach(role -> {
switch (role) {
case "admin":
Role adminRole = roleRepository.findByName(ERole.ROLE_ADMIN)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(adminRole);
break;
case "mod":
Role modRole = roleRepository.findByName(ERole.ROLE_MODERATOR)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(modRole);
break;
default:
Role userRole = roleRepository.findByName(ERole.ROLE_USER)
.orElseThrow(() -> new RuntimeException("Error: Role is not found."));
roles.add(userRole);
}
});
}
user.setRoles(roles);
userRepository.save(user);
return ResponseEntity.ok(new MessageResponse("User registered successfully!"));
}
@PostMapping("/signout")
public ResponseEntity> logoutUser() {
ResponseCookie cookie = jwtUtils.getCleanJwtCookie();
return ResponseEntity.ok().header(HttpHeaders.SET_COOKIE, cookie.toString())
.body(new MessageResponse("You've been signed out!"));
}
}
用于测试授权的控制器
有 4 个 API:
–/api/test/all
用于公共访问
–/api/test/user
用户拥有ROLE_USER
或ROLE_MODERATOR
或ROLE_ADMIN
–/api/test/mod
用户拥有ROLE_MODERATOR
–/api/test/admin
用户拥有ROLE_ADMIN
你还记得我们用来@EnableGlobalMethodSecurity(prePostEnabled = true)
上课WebSecurityConfig
吗?
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... }
现在我们可以@PreAuthorize
轻松地使用注释来保护我们的 API 中的方法。
控制器/TestController.java
package com.bezkoder.spring.security.login.controllers;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/test")
public class TestController {
@GetMapping("/all")
public String allAccess() {
return "Public Content.";
}
@GetMapping("/user")
@PreAuthorize("hasRole('USER') or hasRole('MODERATOR') or hasRole('ADMIN')")
public String userAccess() {
return "User Content.";
}
@GetMapping("/mod")
@PreAuthorize("hasRole('MODERATOR')")
public String moderatorAccess() {
return "Moderator Board.";
}
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String adminAccess() {
return "Admin Board.";
}
}
使用以下命令运行 Spring Boot 应用程序: mvn spring-boot:run
让我们使用 url: 来检查 H2 数据库http://localhost:8080/h2-ui
:
点击Connect按钮,我们在模型包中定义的表将在数据库中自动生成。
在将任何角色分配给用户之前,我们还需要在角色表中添加一些行。
运行以下 SQL 插入语句:
INSERT INTO roles(name) VALUES('ROLE_USER');
INSERT INTO roles(name) VALUES('ROLE_MODERATOR');
INSERT INTO roles(name) VALUES('ROLE_ADMIN');
然后检查角色表:
/signup
使用API注册一些用户:
ROLE_ADMIN
ROLE_MODERATOR
和_ROLE_USER
ROLE_USER
检查用户和user_roles表:
访问公共资源: GET/api/test/all
无需登录即可访问受保护的资源: GET/api/test/user
登录一个帐户: POST/api/auth/signin
检查 Cookie:
访问ROLE_USER
和ROLE_MODERATOR
资源:
– GET /api/test/user
– GET/api/test/mod
访问ROLE_ADMIN
资源: GET /api/test/admin
,响应将是403 Forbidden:
注销帐户: POST/api/auth/signout
如果您使用 JDK 14 运行此 Spring Boot 应用程序并在尝试进行身份验证时出现以下错误:
FilterChain java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
只需将以下依赖项添加到pom.xml:
jakarta.xml.bind
jakarta.xml.bind-api
2.3.2
一切都会好起来的。
今天我们学习了很多关于 Spring Boot 安全登录和注册示例的有趣知识,其中 JWT 和 H2 数据库和 HttpOnly Cookie。
为了更深入地理解架构并更容易掌握概述:
Spring Boot Architecture for JWT with Spring Security
你应该继续了解如何实现 Refresh Token:
Spring Boot Refresh Token with JWT example
您还可以通过本教程了解如何在 AWS(免费)上部署 Spring Boot 应用程序。
快乐学习!再见。
相关文章:
部署:
全栈 CRUD 应用程序:
– Vue + Spring Boot 示例
– Angular 8 + Spring Boot 示例
– Angular 10 + Spring Boot 示例
– Angular 11 + Spring Boot 示例
– Angular 12 + Spring Boot 示例
– Angular 13 + Spring Boot 示例
– React + Spring Boot例子
如果您需要此后端的工作前端,您可以在帖子中找到客户端应用程序:(
只需使用本地存储修改为 Cookie)
– Vue
– Angular 8 / Angular 10 / Angular 11 / Angular 12 / Angular 13
– React / React Hooks / React + Redux
其他数据库:
–使用 JWT 和 MySQL 的
Spring Boot 登录和注册示例 – 使用 JWT 和 MongoDB 的 Spring Boot 登录和注册示例
您可以在Github上找到本教程的完整源代码。