This is the 10th day of my participation in the August More Text Challenge
preface
We know that in the project development, background development authority authentication is very important, Springboot commonly familiar authority authentication framework, Shiro, and is springboot family bucket security of course, they each have their own benefits, But I prefer springBoot’s built-in authorization framework
<! Springboot permission authentication -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Copy the code
Natural integration with Springboot, powerful
Quick learning
The main implementation of Spring Security Security authentication, combined with RESTful API style, using stateless environment.
The main implementation is through the URL of the request, through the filter to do different authorization policy operations, provide a method of authentication for the request, and then authentication, authorization successfully returned to the authorization instance information for service invocation.
Token-based authentication is performed as follows:
A user sends requests using a user name and password. Program validation. The program returns a signed token to the client. The client stores tokens and uses them each time to send a request. The server validates the token and returns data. Every request requires a token, so every request authenticates the user, so you have to use caching,
The flow chart
JWT JSON Web Token verification flowchart
Add Spring Security and JWT dependencies
<! Springboot permission authentication -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<! - JWT certification - >
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
Copy the code
Generate JWT toke
I wrote a utility class JwtTokenUtil to generate JWT Toke
package cn.soboys.kmall.security.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
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;
/ * * *@author kenx
* @version 1.0
* @date2021/8/5 "*@webSite https://www.soboys.cn/
*/
@Component
public class JwtTokenUtil implements Serializable {
private static final long serialVersionUID = -2550185165626007488L;
public static final long JWT_TOKEN_VALIDITY = 7*24*60*60;
private String secret="TcUF7CC8T3txmfQ38pYsQ3KY";
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public String generateToken(String username) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, username);
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return(username.equals(userDetails.getUsername()) && ! isTokenExpired(token)); }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(new Date());
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder().setClaims(claims).setSubject(subject).setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY*1000)).signWith(SignatureAlgorithm.HS512, secret).compact(); }}Copy the code
Injection data source
Spring Security provides the UserDetailsService interface for user authentication, and the UserDetails entity class for storing user information, (user credentials, Permissions, etc.)
Look at the source code
package org.springframework.security.core.userdetails;
public interface UserDetailsService {
UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException;
}
Copy the code
package org.springframework.security.core.userdetails;
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword(a);
String getUsername(a);
boolean isAccountNonExpired(a);
boolean isAccountNonLocked(a);
boolean isCredentialsNonExpired(a);
boolean isEnabled(a);
}
Copy the code
So we divided it into two steps:
- Their own
User
The entity class inherits Spring SecurityUserDetails
Save related permission information
package cn.soboys.kmall.security.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.*;
/** * <p> * User table * </p> **@author kenx
* @sinceThe 2021-08-06 * /
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("t_user")
public class User implements Serializable.UserDetails {
private static final long serialVersionUID = 1L;
/** * user ID */
@TableId(value = "USER_ID", type = IdType.AUTO)
private Long userId;
/** * User name */
@TableField("USERNAME")
private String username;
/** * Password */
@TableField("PASSWORD")
private String password;
/** * Department ID */
@TableField("DEPT_ID")
private Long deptId;
/** * email */
@TableField("EMAIL")
private String email;
/** ** Contact number */
@TableField("MOBILE")
private String mobile;
/**
* 状态 0锁定 1有效
*/
@TableField("STATUS")
private String status;
/** * create time */
@TableField("CREATE_TIME")
private Date createTime;
/** * Change the time */
@TableField("MODIFY_TIME")
private Date modifyTime;
/**
* 最近访问时间
*/
@TableField("LAST_LOGIN_TIME")
private Date lastLoginTime;
/** * Gender 0 male 1 female 2 Confidential */
@TableField("SSEX")
private String ssex;
/** * Whether to enable TAB, 0 to disable 1 to enable */
@TableField("IS_TAB")
private String isTab;
/** * theme */
@TableField("THEME")
private String theme;
/** ** avatar */
@TableField("AVATAR")
private String avatar;
/** * description */
@TableField("DESCRIPTION")
private String description;
@TableField(exist = false)
private List<Role> roles;
@TableField(exist = false)
private Set<String> perms;
/** * User permission **@return* /
/*@Override public Collection
getAuthorities() { List
auths = new ArrayList<>(); List
roles = this.getRoles(); for (Role role : roles) { auths.add(new SimpleGrantedAuthority(role.getRolePerms())); } return auths; } * /
@Override
@JsonIgnore
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> auths = new ArrayList<>();
Set<String> perms = this.getPerms();
for (String perm : perms) {
// If the perms value is null or a null character, an error is reported
auths.add(new SimpleGrantedAuthority(perm));
}
return auths;
}
@Override
@JsonIgnore
public boolean isAccountNonExpired(a) {
return true;
}
@Override
@JsonIgnore
public boolean isAccountNonLocked(a) {
return true;
}
@Override
@JsonIgnore
public boolean isCredentialsNonExpired(a) {
return true;
}
@Override
@JsonIgnore
public boolean isEnabled(a) {
return true; }}Copy the code
Note that the User account is locked when logging in to the User
Because when the user entity class implements the UserDetails interface, I automatically implement all the abstract methods, and automatically generate the following four methods, which return false by default,
@Override
public boolean isAccountNonExpired(a) {
return false;
}
@Override
public boolean isAccountNonLocked(a) {
return false;
}
@Override
public boolean isCredentialsNonExpired(a) {
return false;
}
@Override
public boolean isEnabled(a) {
return false;
}
Copy the code
That’s why, just change their return value to true.
An explanation of several fields in UserDetails:
// Returns the authentication user password, or NULL if it cannot be returned
String getPassword();
String getUsername();
Copy the code
The account is expired. Expiration cannot be verified
boolean isAccountNonExpired();
Copy the code
Specifies whether the user is locked or unlocked. The locked user cannot be authenticated
Boolean isAccountNonLocked ();Copy the code
Credentials (password) indicating whether the user has expired. Expired credentials prevent authentication
boolean isCredentialsNonExpired();
Copy the code
Disabled users cannot be authenticated
boolean isEnabled();
Copy the code
- Implementation interface
loadUserByUsername
Methods inject data validation
Its own IUserService user interface class inherits Spring Security to provide the UserDetailsService interface
public interface IUserService extends IService<User>, UserDetailsService {
User getUserByUsername(String username);
/ * * //** * Obtain all user permissions **@param username
* @return* //* Set
getUserPerms(String username); * /
}
Copy the code
And implement it
@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceImpl extends ServiceImpl<UserMapper.User> implements IUserService {
private final RoleMapper roleMapper;
@Override
public User getUserByUsername(String username) {
return this.baseMapper.selectOne(new QueryWrapper<User>().lambda()
.eq(User::getUsername, username));
}
/** ** when authenticating user details provided by the user@param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = this.getUserByUsername(username);
if (StrUtil.isBlankIfStr(user)) {
throw new UsernameNotFoundException("User not found with username: " + username);
}
// Get user role information
List<Role> roles = roleMapper.findUserRolePermsByUserName(username);
user.setRoles(roles);
List<String> permList = this.baseMapper.findUserPerms(username);
/ / java8 stream is very convenient
Set<String> perms = permList.stream().filter(o->StrUtil.isNotBlank(o)).collect(Collectors.toSet());
user.setPerms(perms);
// Used to add user permissions. Just add user rights to authorities and you'll be fine.
// List<SimpleGrantedAuthority> authorities = new ArrayList<>();
// Used to add user permissions. Just add user rights to authorities and you'll be fine.
/*for (Role role : roles) { authorities.add(new SimpleGrantedAuthority(role.getRolePerms())); log.info("loadUserByUsername: " + user); } * /
//user.setAuthorities(authorities); // Used for the value of the @authenticationPrincipal tag at login
returnuser; }}Copy the code
The loadUserByUsername implementation authenticates the user name and password from the database and obtains user role permission information
Interceptor configuration
Spring Security’s AuthenticationEntryPoint class, which rejects every unauthenticated request and sends error code 401
package cn.soboys.kmall.security.config;
import cn.soboys.kmall.common.ret.Result;
import cn.soboys.kmall.common.ret.ResultCode;
import cn.soboys.kmall.common.ret.ResultResponse;
import cn.soboys.kmall.common.utils.ResponseUtil;
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.Serializable;
/ * * *@author kenx
* @version 1.0
* @date 2021/8/5 22:30
* @webSitehttps://www.soboys.cn/ * This class inherits Spring Security's AuthenticationEntryPoint class, and * rewrites it. It rejects every unauthenticated request and sends the error code 401. * /
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint.Serializable {
/** * This class inherits Spring Security's AuthenticationEntryPoint class and re-writes commence. * It rejects every unauthenticated request and sends the error code 401 * *@param httpServletRequest
* @param httpServletResponse
* @param e
* @throws IOException
* @throws ServletException
*/
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
Result result = ResultResponse.failure(ResultCode.UNAUTHORIZED, "Please log in first."); ResponseUtil.responseJson(httpServletResponse, result); }}Copy the code
JwtRequestFilter Any request performs this kind of check to see if the request has a valid JWT token. If it has a valid JWT token, it sets Authentication in the context to specify that the current user is authenticated.
package cn.soboys.kmall.security.config;
import cn.soboys.kmall.common.utils.ConstantFiledUtil;
import cn.soboys.kmall.security.service.IUserService;
import cn.soboys.kmall.security.utils.JwtTokenUtil;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
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.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;
/ * * *@author kenx
* @version 1.0
* @date2021/8/5 her *@webSitehttps://www.soboys.cn/ * Any request will perform this class * to check if the request has a valid JWT token. If it has a valid JWT token, * then it sets Authentication in the context to specify that the current user is authenticated. * /
@Component
@Slf4j
public class JwtRequestFilter extends OncePerRequestFilter {
// User data source
private IUserService userService;
// Generate JWT tokens
private JwtTokenUtil jwtTokenUtil;
public JwtRequestFilter(IUserService userService,JwtTokenUtil jwtTokenUtil) {
this.userService = userService;
this.jwtTokenUtil = jwtTokenUtil;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String requestTokenHeader = request.getHeader(ConstantFiledUtil.AUTHORIZATION_TOKEN);
String username = null;
String jwtToken = null;
// JWT Token is in the form "Bearer token". Remove Bearer word and get only the Token
if(requestTokenHeader ! =null && requestTokenHeader.startsWith("Bearer ")) {
jwtToken = requestTokenHeader.substring(7);
try {
username = jwtTokenUtil.getUsernameFromToken(jwtToken);
} catch (IllegalArgumentException e) {
log.error("Unable to get JWT Token");
} catch (ExpiredJwtException e) {
log.error("JWT Token has expired"); }}else {
//logger.warn("JWT Token does not begin with Bearer String");
}
//Once we get the token validate it.
if(username ! =null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = this.userService.loadUserByUsername(username);
// if token is valid configure Spring Security to manually set authentication
if (jwtTokenUtil.validateToken(jwtToken, userDetails)) {
// Save user information and permission information
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken
.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
// After setting the Authentication in the context, we specify
// that the current user is authenticated. So it passes the Spring Security Configurations successfully.SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } } filterChain.doFilter(request, response); }}Copy the code
Configure the Spring Security configuration class SecurityConfig
-
Custom Spring Security when we need to inherit from WebSecurityConfigurerAdapter to complete, relevant configuration rewrite the corresponding method
-
The BCryptPasswordEncoder password is used for encryption
-
Add our own custom authentication by overriding the configure method.
package cn.soboys.kmall.security.config;
import cn.soboys.kmall.security.service.IUserService;
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.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 java.util.Set;
/ * * *@author kenx
* @version 1.0
* @date 2021/8/6 17:27
* @webSite https://www.soboys.cn/
*/
@EnableWebSecurity
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true) // Control permission annotations
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private IUserService userService;
private JwtRequestFilter jwtRequestFilter;
public SecurityConfig(JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint, IUserService userService, JwtRequestFilter jwtRequestFilter) {
this.jwtAuthenticationEntryPoint = jwtAuthenticationEntryPoint;
this.userService = userService;
this.jwtRequestFilter = jwtRequestFilter;
}
/** * 1) HttpSecurity supports CORS. * 2) CRSF is enabled by default. Because the thymeleaf template is not used (_cSRf parameter will be automatically injected), * you need to disable CSRF first. Otherwise, the _cSRF parameter will be required during login, resulting in login failure. * 3) antMatchers: match "/" path, do not need permission to access, match "/user" and all paths below, * all need "user" permission * 4) configure login address and exit address */
@Override
protected void configure(HttpSecurity http) throws Exception {
// We don't need CSRF for this example
http.csrf().disable()
// dont authenticate this particular request
.authorizeRequests().antMatchers("/"."/*.html"."/favicon.ico"."/css/**"."/js/**"."/fonts/**"."/layui/**"."/img/**"."/v3/api-docs/**"."/swagger-resources/**"."/webjars/**"."/pages/**"."/druid/**"."/statics/**"."/login"."/register").permitAll().
// all other requests need to be authenticated
anyRequest().authenticated().and().
// make sure we use stateless session; session won't be used to
// store user's state.
// Override the default login
exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
// Token-based, so no session is required
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
@Bean
@Override
public AuthenticationManager authenticationManagerBean(a) throws Exception {
return super.authenticationManagerBean();
}
/** * Password verification **@param auth
* @throws Exception
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
// configure AuthenticationManager so that it knows from where to load
// user for matching credentials
// Use BCryptPasswordEncoder
auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
}
/** * Password encryption verification **@return* /
@Bean
public PasswordEncoder passwordEncoder(a) {
return newBCryptPasswordEncoder(); }}Copy the code
The specific application
package cn.soboys.kmall.security.controller;
import cn.hutool.core.util.StrUtil;
import cn.soboys.kmall.common.exception.BusinessException;
import cn.soboys.kmall.common.ret.ResponseResult;
import cn.soboys.kmall.common.ret.Result;
import cn.soboys.kmall.common.ret.ResultResponse;
import cn.soboys.kmall.security.entity.User;
import cn.soboys.kmall.security.service.IUserService;
import cn.soboys.kmall.security.utils.EncryptPwdUtil;
import cn.soboys.kmall.security.utils.JwtTokenUtil;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import io.swagger.annotations.*;
import lombok.RequiredArgsConstructor;
import lombok.SneakyThrows;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import java.util.Date;
import java.util.Objects;
/ * * *@author kenx
* @version 1.0
* @date 2021/8/6 12:30
* @webSite https://www.soboys.cn/
*/
@RestController
@ResponseResult
@Validated
@RequiredArgsConstructor
@API (tags = "login interface ")
public class LoginController {
private final IUserService userService;
// Authentication management, authentication user province
private final AuthenticationManager authenticationManager;
private final JwtTokenUtil jwtTokenUtil;
// Own data source
private final UserDetailsService jwtInMemoryUserDetailsService;
@PostMapping("/login")
@apiOperation (" User login ")
@SneakyThrows
public Result login(@NotBlank @RequestParam String username,
@NotBlank @RequestParam String password) {
Authentication authentication= this.authenticate(username, password);
String user = authentication.getName();
final String token = jwtTokenUtil.generateToken(user);
// Update the last login time of the user
User u = new User();
u.setLastLoginTime(new Date());
userService.update(u, new UpdateWrapper<User>().lambda().eq(User::getUsername, username));
return ResultResponse.success("Bearer " + token);
}
@PostMapping("/register")
@apiOperation (" User registration ")
public Result register(@NotEmpty @RequestParam String username, @NotEmpty @RequestParam String password) {
User user = userService.getUserByUsername(username);
if(! StrUtil.isBlankIfStr(user)) {throw new BusinessException("User already exists");
}
User u = new User();
u.setPassword(EncryptPwdUtil.encryptPassword(password));
u.setUsername(username);
u.setCreateTime(new Date());
u.setModifyTime(new Date());
u.setStatus("1");
userService.save(u);
return ResultResponse.success();
}
private Authentication authenticate(String username, String password) throws Exception {
Authentication authentication = null;
try {
//security Authenticates the user
authentication = authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (DisabledException e) {
throw new BusinessException("User does not save");
} catch (BadCredentialsException e) {
throw new BusinessException("Username and password error");
}
returnauthentication; }}Copy the code
Insight into the
Spring Security configuration description
- @enablewebSecurity Enables permission authentication
- @ EnableGlobalMethodSecurity open authority annotation certification (prePostEnabled = true)
- Configure the configuration
@Override
protected void configure(HttpSecurity http) throws Exception {
// We don't need CSRF for this example
http.csrf().disable()
// dont authenticate this particular request
.authorizeRequests().antMatchers("/"."/*.html"."/favicon.ico"."/css/**"."/js/**"."/fonts/**"."/layui/**"."/img/**"."/v3/api-docs/**"."/swagger-resources/**"."/webjars/**"."/pages/**"."/druid/**"."/statics/**"."/login"."/register").permitAll().
// all other requests need to be authenticated
anyRequest().authenticated().and().
// make sure we use stateless session; session won't be used to
// store user's state.
// Override the default login
exceptionHandling().authenticationEntryPoint(jwtAuthenticationEntryPoint).and().sessionManagement()
// Token-based, so no session is required
.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
// Add a filter to validate the tokens with every request
http.addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class);
}
Copy the code
reference
- Spring Security permission authentication
- HTTP Basic authentication for springSecurity
- SpringBoot Security + JWT Hello World