Feature: JWT + Spring Security 实现用户权限认证.

pull/10/head
chen.ma 3 years ago
parent 1730efafca
commit 9400439700

@ -53,6 +53,22 @@
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>io.github.acmenlt</groupId>
<artifactId>common</artifactId>
</dependency>
</dependencies>
</project>

@ -0,0 +1,81 @@
package com.github.dynamic.threadpool.auth.config;
import com.github.dynamic.threadpool.auth.constant.Constants;
import com.github.dynamic.threadpool.auth.filter.JWTAuthenticationFilter;
import com.github.dynamic.threadpool.auth.filter.JWTAuthorizationFilter;
import com.github.dynamic.threadpool.auth.service.impl.UserDetailsServiceImpl;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import javax.annotation.Resource;
/**
* .
*
* @author chen.ma
* @date 2021/11/9 21:10
*/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class GlobalSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource
private UserDetailsService userDetailsService;
@Bean
public UserDetailsService customUserService() {
return new UserDetailsServiceImpl();
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.addAllowedMethod(Constants.SPLIT_STAR);
config.applyPermitDefaultValues();
source.registerCorsConfiguration("/**", config);
return source;
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers("/static/**", "/index.html", "/favicon.ico", "/avatar.jpg").permitAll()
.antMatchers("/doc.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs").anonymous()
.anyRequest().authenticated()
.and()
.addFilter(new JWTAuthenticationFilter(authenticationManager()))
.addFilter(new JWTAuthorizationFilter(authenticationManager()))
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/v1/cs/apps/renew/**", "/v1/cs/apps/register/**", "/v1/cs/configs/**", "/v1/cs/listener/**");
}
}

@ -0,0 +1,15 @@
package com.github.dynamic.threadpool.auth.constant;
/**
* Constants.
*
* @author chen.ma
* @date 2021/11/9 22:24
*/
public class Constants {
public static final String SPLIT_STAR = "*";
public static final String SPLIT_COMMA = ",";
}

@ -0,0 +1,93 @@
package com.github.dynamic.threadpool.auth.filter;
import cn.hutool.json.JSONUtil;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.github.dynamic.threadpool.auth.model.biz.user.JwtUser;
import com.github.dynamic.threadpool.auth.model.biz.user.LoginUser;
import com.github.dynamic.threadpool.auth.toolkit.JwtTokenUtil;
import com.github.dynamic.threadpool.auth.toolkit.ReturnT;
import com.github.dynamic.threadpool.common.web.base.Results;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import static com.github.dynamic.threadpool.auth.constant.Constants.SPLIT_COMMA;
/**
* JWT authentication filter.
*
* @author chen.ma
* @date 2021/11/9 22:21
*/
@Slf4j
public class JWTAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private AuthenticationManager authenticationManager;
private ThreadLocal<Integer> rememberMe = new ThreadLocal();
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
super.setFilterProcessesUrl("/v1/cs/auth/login");
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 从输入流中获取到登录的信息
try {
LoginUser loginUser = new ObjectMapper().readValue(request.getInputStream(), LoginUser.class);
rememberMe.set(loginUser.getRememberMe());
return authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginUser.getUsername(), loginUser.getPassword(), new ArrayList<>())
);
} catch (IOException e) {
logger.error("attemptAuthentication error :{}", e);
return null;
}
}
@Override
protected void successfulAuthentication(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain,
Authentication authResult) throws IOException {
JwtUser jwtUser = (JwtUser) authResult.getPrincipal();
boolean isRemember = rememberMe.get() == 1;
String role = "";
Collection<? extends GrantedAuthority> authorities = jwtUser.getAuthorities();
for (GrantedAuthority authority : authorities) {
role = authority.getAuthority();
}
String token = JwtTokenUtil.createToken(jwtUser.getId(), jwtUser.getUsername(), role, isRemember);
response.setHeader("token", JwtTokenUtil.TOKEN_PREFIX + token);
response.setCharacterEncoding("UTF-8");
Map<String, Object> maps = new HashMap<>();
maps.put("data", JwtTokenUtil.TOKEN_PREFIX + token);
maps.put("roles", role.split(SPLIT_COMMA));
response.getWriter().write(JSONUtil.toJsonStr(Results.success(maps)));
}
@Override
protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
response.setCharacterEncoding("UTF-8");
response.getWriter().write(JSONUtil.toJsonStr(new ReturnT(-1, "Server Error")));
}
}

@ -0,0 +1,83 @@
package com.github.dynamic.threadpool.auth.filter;
import com.alibaba.fastjson.JSON;
import com.github.dynamic.threadpool.auth.toolkit.JwtTokenUtil;
import com.github.dynamic.threadpool.common.web.base.Results;
import com.github.dynamic.threadpool.common.web.exception.ServiceException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Collections;
/**
* JWT authorization filter.
*
* @author chen.ma
* @date 2021/11/9 22:21
*/
@Slf4j
public class JWTAuthorizationFilter extends BasicAuthenticationFilter {
public JWTAuthorizationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain chain) throws IOException, ServletException {
String tokenHeader = request.getHeader(JwtTokenUtil.TOKEN_HEADER);
// 如果请求头中没有 Authorization 信息则直接放行
if (tokenHeader == null || !tokenHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) {
chain.doFilter(request, response);
return;
}
// 如果请求头中有 Token, 则进行解析, 并且设置认证信息
try {
SecurityContextHolder.getContext().setAuthentication(getAuthentication(tokenHeader));
} catch (Exception ex) {
// 返回 Json 形式的错误信息
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.getWriter().write(JSON.toJSONString(Results.failure("-1", ex.getMessage())));
response.getWriter().flush();
return;
}
super.doFilterInternal(request, response, chain);
}
/**
* Token Token.
*
* @param tokenHeader
* @return
*/
private UsernamePasswordAuthenticationToken getAuthentication(String tokenHeader) {
String token = tokenHeader.replace(JwtTokenUtil.TOKEN_PREFIX, "");
boolean expiration = JwtTokenUtil.isExpiration(token);
if (expiration) {
throw new ServiceException("登录时间过长,请退出重新登录");
}
String username = JwtTokenUtil.getUsername(token);
String role = JwtTokenUtil.getUserRole(token);
if (username != null) {
return new UsernamePasswordAuthenticationToken(username, null,
Collections.singleton(new SimpleGrantedAuthority(role))
);
}
return null;
}
}

@ -31,6 +31,11 @@ public class UserInfo {
*/
private String password;
/**
* role
*/
private String role;
/**
* gmtCreate
*/

@ -0,0 +1,58 @@
package com.github.dynamic.threadpool.auth.model.biz.user;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
/**
* Jwt user.
*
* @author chen.ma
* @date 2021/11/9 22:34
*/
@Data
public class JwtUser implements UserDetails {
/**
* id
*/
private Long id;
/**
* userName
*/
private String username;
/**
* password
*/
private String password;
/**
* authorities
*/
private Collection<? extends GrantedAuthority> authorities;
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}

@ -0,0 +1,29 @@
package com.github.dynamic.threadpool.auth.model.biz.user;
import lombok.Data;
/**
* Login user.
*
* @author chen.ma
* @date 2021/11/9 22:41
*/
@Data
public class LoginUser {
/**
* username
*/
private String username;
/**
* password
*/
private String password;
/**
* rememberMe
*/
private Integer rememberMe;
}

@ -12,8 +12,9 @@ import lombok.Data;
@Data
public class UserQueryPageReqDTO extends Page {
public UserQueryPageReqDTO(long current, long size) {
super(current, size);
}
/**
* userName
*/
private String userName;
}

@ -0,0 +1,30 @@
package com.github.dynamic.threadpool.auth.model.biz.user;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import lombok.Data;
/**
* User req dto.
*
* @author chen.ma
* @date 2021/11/11 20:30
*/
@Data
public class UserReqDTO extends Page {
/**
* userName
*/
private String userName;
/**
* password
*/
private String password;
/**
* role
*/
private String role;
}

@ -20,9 +20,9 @@ public class UserRespDTO {
private String userName;
/**
* password
* role
*/
private String password;
private String role;
/**
* gmtCreate

@ -1,6 +1,8 @@
package com.github.dynamic.threadpool.auth.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.github.dynamic.threadpool.auth.model.biz.user.UserQueryPageReqDTO;
import com.github.dynamic.threadpool.auth.model.biz.user.UserReqDTO;
import com.github.dynamic.threadpool.auth.model.biz.user.UserRespDTO;
import java.util.List;
@ -16,27 +18,24 @@ public interface UserService {
/**
* .
*
* @param pageNo
* @param pageSize
* @param reqDTO
* @return
*/
IPage<UserRespDTO> listUser(int pageNo, int pageSize);
IPage<UserRespDTO> listUser(UserQueryPageReqDTO reqDTO);
/**
* .
*
* @param userName
* @param password
* @param reqDTO
*/
void addUser(String userName, String password);
void addUser(UserReqDTO reqDTO);
/**
* .
*
* @param userName
* @param password
* @param reqDTO
*/
void updateUser(String userName, String password);
void updateUser(UserReqDTO reqDTO);
/**
* .

@ -0,0 +1,42 @@
package com.github.dynamic.threadpool.auth.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.dynamic.threadpool.auth.mapper.UserMapper;
import com.github.dynamic.threadpool.auth.model.UserInfo;
import com.github.dynamic.threadpool.auth.model.biz.user.JwtUser;
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 javax.annotation.Resource;
import java.util.Collections;
import java.util.Set;
/**
* User details service impl.
*
* @author chen.ma
* @date 2021/11/9 22:26
*/
public class UserDetailsServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
UserInfo userInfo = userMapper.selectOne(Wrappers.lambdaQuery(UserInfo.class).eq(UserInfo::getUserName, userName));
JwtUser jwtUser = new JwtUser();
jwtUser.setId(userInfo.getId());
jwtUser.setUsername(userName);
jwtUser.setPassword(userInfo.getPassword());
Set<SimpleGrantedAuthority> authorities = Collections.singleton(new SimpleGrantedAuthority(userInfo.getRole() + ""));
jwtUser.setAuthorities(authorities);
return jwtUser;
}
}

@ -1,7 +1,7 @@
package com.github.dynamic.threadpool.auth.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
@ -9,10 +9,12 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.github.dynamic.threadpool.auth.mapper.UserMapper;
import com.github.dynamic.threadpool.auth.model.UserInfo;
import com.github.dynamic.threadpool.auth.model.biz.user.UserQueryPageReqDTO;
import com.github.dynamic.threadpool.auth.model.biz.user.UserReqDTO;
import com.github.dynamic.threadpool.auth.model.biz.user.UserRespDTO;
import com.github.dynamic.threadpool.auth.service.RoleService;
import com.github.dynamic.threadpool.auth.service.UserService;
import lombok.AllArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@ -32,39 +34,39 @@ public class UserServiceImpl implements UserService {
private final RoleService roleService;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
@Override
public IPage<UserRespDTO> listUser(int pageNo, int pageSize) {
UserQueryPageReqDTO queryPage = new UserQueryPageReqDTO(pageNo, pageSize);
IPage<UserInfo> selectPage = userMapper.selectPage(queryPage, null);
public IPage<UserRespDTO> listUser(UserQueryPageReqDTO reqDTO) {
IPage<UserInfo> selectPage = userMapper.selectPage(reqDTO, null);
return selectPage.convert(each -> BeanUtil.toBean(each, UserRespDTO.class));
}
@Override
public void addUser(String userName, String password) {
public void addUser(UserReqDTO reqDTO) {
LambdaQueryWrapper<UserInfo> queryWrapper = Wrappers.lambdaQuery(UserInfo.class)
.eq(UserInfo::getUserName, userName);
.eq(UserInfo::getUserName, reqDTO.getUserName());
UserInfo existUserInfo = userMapper.selectOne(queryWrapper);
if (existUserInfo != null) {
throw new RuntimeException("用户名重复");
}
UserInfo insertUser = new UserInfo();
insertUser.setUserName(userName);
// TODO 暂定为 Md5 加密
insertUser.setPassword(SecureUtil.md5(password));
reqDTO.setPassword(bCryptPasswordEncoder.encode(reqDTO.getPassword()));
UserInfo insertUser = BeanUtil.toBean(reqDTO, UserInfo.class);
userMapper.insert(insertUser);
}
@Override
public void updateUser(String userName, String password) {
UserInfo userInfo = new UserInfo();
userInfo.setUserName(userName);
userInfo.setPassword(SecureUtil.md5(password));
public void updateUser(UserReqDTO reqDTO) {
if (StrUtil.isNotBlank(reqDTO.getPassword())) {
reqDTO.setPassword(bCryptPasswordEncoder.encode(reqDTO.getPassword()));
}
UserInfo updateUser = BeanUtil.toBean(reqDTO, UserInfo.class);
LambdaUpdateWrapper<UserInfo> updateWrapper = Wrappers.lambdaUpdate(UserInfo.class)
.eq(UserInfo::getUserName, userName);
userMapper.update(userInfo, updateWrapper);
.eq(UserInfo::getUserName, reqDTO.getUserName());
userMapper.update(updateUser, updateWrapper);
}
@Override
@ -72,7 +74,7 @@ public class UserServiceImpl implements UserService {
LambdaUpdateWrapper<UserInfo> updateWrapper = Wrappers.lambdaUpdate(UserInfo.class)
.eq(UserInfo::getUserName, userName);
userMapper.delete(updateWrapper);
roleService.deleteRole("", userName);
// roleService.deleteRole("", userName);
}
@Override

@ -0,0 +1,120 @@
package com.github.dynamic.threadpool.auth.toolkit;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import static com.github.dynamic.threadpool.auth.constant.Constants.SPLIT_COMMA;
/**
* Jwt token util.
*
* @author chen.ma
* @date 2021/11/9 22:43
*/
public class JwtTokenUtil {
public static final String TOKEN_HEADER = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
private static final String SECRET = "Hippo4J_admin";
private static final String ISS = "admin";
/**
* Key
*/
private static final String ROLE_CLAIMS = "rol";
/**
* 3600 , 24
*/
private static final long EXPIRATION = 86400L;
/**
* 7
*/
private static final long EXPIRATION_REMEMBER = 7 * EXPIRATION;
/**
* Token.
*
* @param id
* @param username
* @param role
* @param isRememberMe
* @return
*/
public static String createToken(Long id, String username, String role, boolean isRememberMe) {
long expiration = isRememberMe ? EXPIRATION_REMEMBER : EXPIRATION;
HashMap<String, Object> map = new HashMap<>();
map.put(ROLE_CLAIMS, role);
return Jwts.builder()
.signWith(SignatureAlgorithm.HS512, SECRET)
.setClaims(map)
.setIssuer(ISS)
.setSubject(id + SPLIT_COMMA + username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
.compact();
}
/**
* Token .
*
* @param token
* @return
*/
public static String getUsername(String token) {
List<String> userInfo = Arrays.asList(getTokenBody(token).getSubject().split(SPLIT_COMMA));
return userInfo.get(1);
}
/**
* Token .
*
* @param token
* @return
*/
public static Integer getUserId(String token) {
List<String> userInfo = Arrays.asList(getTokenBody(token).getSubject().split(SPLIT_COMMA));
return Integer.parseInt(userInfo.get(0));
}
/**
* .
*
* @param token
* @return
*/
public static String getUserRole(String token) {
return (String) getTokenBody(token).get(ROLE_CLAIMS);
}
/**
* .
*
* @param token
* @return
*/
public static boolean isExpiration(String token) {
try {
return getTokenBody(token).getExpiration().before(new Date());
} catch (ExpiredJwtException e) {
return true;
}
}
private static Claims getTokenBody(String token) {
return Jwts.parser()
.setSigningKey(SECRET)
.parseClaimsJws(token)
.getBody();
}
}

@ -0,0 +1,40 @@
package com.github.dynamic.threadpool.auth.toolkit;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* ReturnT.
*
* @author chen.ma
* @date 2021/11/10 00:00
*/
@Data
@NoArgsConstructor
public class ReturnT<T> implements Serializable {
public static final long serialVersionUID = 42L;
public static final int SUCCESS_CODE = 200;
public static final int FAIL_CODE = 500;
public static final ReturnT<String> SUCCESS = new ReturnT<>(null);
public static final ReturnT<String> FAIL = new ReturnT<>(FAIL_CODE, null);
private int code;
private String msg;
private T content;
public ReturnT(int code, String msg) {
this.code = code;
this.msg = msg;
}
public ReturnT(T content) {
this.code = SUCCESS_CODE;
this.content = content;
}
}

@ -37,6 +37,7 @@
<hibernate-validator.version>6.1.5.Final</hibernate-validator.version>
<transmittable-thread-local.version>2.12.1</transmittable-thread-local.version>
<forest.version>1.5.11</forest.version>
<jjwt.version>0.9.0</jjwt.version>
<dingtalk-sdk.version>1.0.1</dingtalk-sdk.version>
@ -202,7 +203,7 @@
<build>
<finalName>${project.artifactId}</finalName>
<resources>
<!--<resources>
<resource>
<directory>src/main/resources</directory>
<includes>
@ -213,7 +214,7 @@
</excludes>
<filtering>true</filtering>
</resource>
</resources>
</resources>-->
<plugins>
<plugin>

Loading…
Cancel
Save