作者简介:
🌹 作者:暗夜91
🤟 个人主页:暗夜91的主页
📝 如果感觉文章写的还有点帮助,请帮忙点个关注,我会持续输出高质量技术博文。
专栏文章:
1、集成Swagger,生成API文档
2、Mysql数据源配置
3、集成Redis
4、Spring Security + JWT实现登录权限认证
5、跨域配置
专栏源码:
针对该专栏功能,对源码进行整理,可以直接下载运行。
源码下载请移步:SpringBoot快速开发框架
四、Spring Security + JWT实现登录权限认证
1、Spring Security介绍
Spring Security是一个高度自定义的安全框架,利用Spring IOC和AOP的功能,为系统提供声明式的安全访问控制。
Spring Security最重要的核心功能就是**【认证】和【授权】**,认证通俗的说就是判断用户是否成功登录。授权则是判断用户是否有权限去访问业务接口。
2、JWT
Json Web Token (JWT):为了在网络应用环境间传递声明而执行的一种基于JSON的开发标准(RFC97519),它定义 了一种紧凑的、自包含的方式,用于作为JSON对象安全的传输信息,是一种数字签名的方式。
JWT最常见的应用场景就是授权,一旦对用户进行授权,返回用户授权令牌(token),之后用户的所有请求必须携带token,用于对用户的权限进行验证。
JWT的结构
JWT有三部分组成,他们之间用圆点(.)连接,这三部分分别是:
- header
header 由两部分组成,token的类型(JWT)和算法名称(比如SHA256或者RSA等),用BASE64对header进行编码就得到了JWT的第一部分
- payload
它是JWT的第二部分,里面包含声明(要求),是关于用户或者其他数据的声明,有三种声明类型:registered, public 和 private。
- Registered claims : 这里有一组预定义的声明,它们不是强制的,但是推荐。比如:iss (issuer), exp (expiration time), sub (subject), aud (audience)等。
- Public claims : 可以随意定义。
- Private claims : 用于在同意使用它们的各方之间共享信息,并且不是注册的或公开的声明。
- signature
JWT的签名部分,使用编码过的header和payload,然后通过密钥对信息进行加密签名得到。
3、集成方法
(1)引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
引入依赖后,启动服务可以看到日志中会出现security默认的密码,这里不做过多解释,因为之后会对登录进行自定义实现。
(2)创建用户实体类和对应的JPA接口
这里承接第二部中的数据源配置,使用JPA作为ORM框架
UserEntity
User的实体类,并且要实现UserDetails接口,之后会用到这个地方
package com.lll.framework.module.user.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;
import java.util.Collection;
@Entity
@Table(name = "sys_user_info")
public class UserEntity implements UserDetails {
@Id
@GeneratedValue
private Integer id;
private String username;
private String password;
@Override
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
@Override
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserRepository
Jpa对于User类的查询接口,用于对用户的登录验证使用。
package com.lll.framework.module.user.repository;
import com.lll.framework.module.user.entity.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
public interface UserRepository extends JpaRepository<UserEntity,Integer> {
}
(3)自定义UserDetailsService
创建UserServiceImpl,并实现UserDetailsService,从数据库中获取用户信息,进行权限验证。
package com.lll.framework.module.user.service.impl;
import com.lll.framework.module.user.entity.UserEntity;
import com.lll.framework.module.user.repository.UserRepository;
import com.lll.framework.module.user.service.UserService;
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.util.CollectionUtils;
import java.util.List;
public class UserServiceImpl implements UserService, UserDetailsService {
@Autowired
private UserRepository userRepository;
/**
* 自定义用户验证信息
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
List<UserEntity> userList = userRepository.findByUsername(username);
if (CollectionUtils.isEmpty(userList)){
return new UserEntity();
}
UserEntity userEntity = userList.get(0);
/** TODO
* 这里是使用最小范围返回的用户信息
* 可以加入实际的业务需求,比如用户的角色相关的信息等
*/
return userEntity;
}
}
(4)自定义退出类
package com.lll.framework.module.security.handle;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lll.framework.common.response.Response;
import com.lll.framework.common.response.ResponseUtil;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
public class LogoutSuccessHandlerImpl implements LogoutSuccessHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onLogoutSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
Response response = ResponseUtil.success();
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter out = httpServletResponse.getWriter();
out.write(objectMapper.writeValueAsString(response));
out.flush();
out.close();
}
}
(5)自定义权限验证失败类
package com.lll.framework.module.security.handle;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.lll.framework.common.response.Response;
import org.springframework.beans.factory.annotation.Autowired;
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;
/**
* 认证失败处理类 返回未授权
**/
@Component
public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint {
@Autowired
private ObjectMapper objectMapper;
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
System.out.println(e.getMessage());
Response response = new Response();
response.setCode(300);
response.setMsg("未登陆");
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(403);
httpServletResponse.getWriter().append(objectMapper.writeValueAsString(response));
}
}
(6)新增Jwt工具类
Jwt的工具类,提供了生成token的公共方法。
package com.lll.framework.module.security.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
import java.util.Date;
public class JwtTokenUtil {
private static Key key = new SecretKeySpec(new BCryptPasswordEncoder().encode("lll-framework").getBytes(), SignatureAlgorithm.HS512.getJcaName());
@Value("${server.tokenDuration}")
private int tokenDuration;
public int getTokenDuration() {
return tokenDuration;
}
/**
* 生成token
* @param username
* @param tokenDuration 有效时长
* @return
*/
public String generateToken(String username,int tokenDuration ) {
String token = Jwts.builder()
.setClaims(null)
.setSubject(username)
.setExpiration(new Date(System.currentTimeMillis() + tokenDuration * 1000))
.signWith(SignatureAlgorithm.HS512,key)
.compact() ;
return token;
}
/**
* 解析token,如果已经过期,刷新token
* @param token
* @return
*/
public String parseToken(String token) {
String subject = null;
try {
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token).getBody();
subject = claims.getSubject();
} catch (Exception e) {
System.out.println(e.getLocalizedMessage());
subject="taken无效";
}
return subject;
}
}
(7)新增JwtAuthenticationTokenFilter
JWT全局拦截,对所有的API请求进行拦截,从请求header中获取token信息,并与redis中的缓存信息进行比对验证。
package com.lll.framework.module.security.filter;
import com.alibaba.fastjson.JSON;
import com.lll.framework.common.redis.RedisTemplateService;
import com.lll.framework.module.security.util.JwtTokenUtil;
import com.lll.framework.module.user.entity.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
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;
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplateService redisTemplate;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException {
String authHeader = request.getHeader("Authorization");
if (authHeader != null) {
String authToken;
if (authHeader.contains("Bearer ")) {
authToken = authHeader.substring(7);
} else {
authToken = authHeader;
}
boolean flag = redisTemplate.hasKey(authToken);
if (flag) {
String userInfo = redisTemplate.get(authToken, String.class);
if (SecurityContextHolder.getContext().getAuthentication() == null) {
UserEntity userDetails = JSON.parseObject(userInfo,UserEntity.class);
if (userDetails != null) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
}
chain.doFilter(request, response);
}
}
(8)新增Spring Security配置文件
在配置文件中,对登录请求的验证进行配置,并指定请求的白名单,对白名单中的请求不进行拦截。
package com.lll.framework.config;
import com.lll.framework.module.security.filter.JwtAuthenticationTokenFilter;
import com.lll.framework.module.security.handle.AuthenticationEntryPointImpl;
import com.lll.framework.module.security.handle.LogoutSuccessHandlerImpl;
import com.lll.framework.module.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.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;
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private LogoutSuccessHandlerImpl logoutSuccessHandler;
@Autowired
private AuthenticationEntryPointImpl authenticationEntryPoint;
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// CSRF禁用,因为不使用session
.csrf().disable()
// 认证失败处理类
.exceptionHandling().authenticationEntryPoint(authenticationEntryPoint).and()
// 基于token,所以不需要session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
// 过滤请求
.authorizeRequests()
// 对于登录login 注册register 验证码captchaImage 允许匿名访问
.antMatchers("/sign/login", "/user/register").anonymous()
.antMatchers(
HttpMethod.GET,
"/",
"/*.html",
"/**/*.html",
"/**/*.css",
"/**/*.js",
"/profile/**"
).permitAll()
.antMatchers("/swagger-ui.html").anonymous()
.antMatchers("/swagger-resources/**").anonymous()
.antMatchers("/webjars/**").anonymous()
.antMatchers("/*/api-docs").anonymous()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated()
.and()
.headers().frameOptions().disable();
httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);
// 添加JWT filter
httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
}
/**
* 解决 无法直接注入 AuthenticationManager
*
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
(9)添加登录接口
Controller
package com.lll.framework.module.login.controller;
import com.lll.framework.common.response.Response;
import com.lll.framework.common.response.ResponseUtil;
import com.lll.framework.module.login.service.LoginService;
import com.lll.framework.module.login.vo.LoginVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author: Liu Lili <br>
*/
@Api(tags = "登录")
@RestController
@RequestMapping("/sign")
public class LoginController {
@Autowired
private LoginService loginService;
@ApiOperation("用户名密码登录")
@PostMapping("/login")
public Response loginByUsernameAndPwd(LoginVO loginVO) throws Exception {
String token = loginService.login(loginVO.getUsername(), loginVO.getPassword());
return ResponseUtil.success(token);
}
}
ServiceImpl
package com.lll.framework.module.login.service.impl;
import com.lll.framework.common.redis.RedisTemplateService;
import com.lll.framework.module.login.service.LoginService;
import com.lll.framework.module.security.util.JwtTokenUtil;
import com.lll.framework.module.user.entity.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
@Service
public class LoginServiceImpl implements LoginService {
@Resource
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private RedisTemplateService redisTemplate;
@Override
public String login(String username, String password) throws Exception {
Authentication authentication = null;
try {
// 该方法会去调用UserDetailsServiceImpl.loadUserByUsername
authentication = authenticationManager
.authenticate(new UsernamePasswordAuthenticationToken(username, password));
} catch (Exception e) {
throw new Exception();
}
UserEntity loginUser = (UserEntity) authentication.getPrincipal();
String token = jwtTokenUtil.generateToken(loginUser.getUsername());
redisTemplate.set(token,loginUser);
return token;
}
}
到这里Spring Security的集成就完成了,小伙伴们可以愉快的进行用户的登录和权限验证了,下面j写一个注册接口和一个查询全部用户接口做一下测试。
(10)测试
- 对注册接口需要加入白名单,无需token便可直接访问
- 对查询全部用户接口,则需验证通过才能进行访问
Controller
package com.lll.framework.module.user.controller;
import com.lll.framework.common.response.Response;
import com.lll.framework.common.response.ResponseUtil;
import com.lll.framework.module.user.entity.UserEntity;
import com.lll.framework.module.user.service.UserService;
import com.lll.framework.module.user.vo.UserVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/user")
@Api(tags = "用户管理")
public class UserController {
@Autowired
private UserService userService;
@ApiOperation(value = "用户注册")
@PostMapping("register")
public Response saveUserInfo(@RequestBody UserVO user){
Boolean flag = userService.saveUserInfo(user);
return ResponseUtil.success(flag);
}
@ApiOperation(value = "获取全部用户")
@GetMapping("/list")
public Response getUserList(){
List<UserEntity> list = userService.getUserList();
return ResponseUtil.success(list);
}
}
ServiceImpl
package com.lll.framework.module.user.service.impl;
import com.lll.framework.module.user.entity.UserEntity;
import com.lll.framework.module.user.repository.UserRepository;
import com.lll.framework.module.user.service.UserService;
import com.lll.framework.module.user.vo.UserVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public Boolean saveUserInfo(UserVO user) {
String pwd = user.getPassword();
user.setPassword(passwordEncoder.encode(pwd));
UserEntity entity = new UserEntity();
entity.setUsername(user.getUsername());
entity.setPassword(user.getPassword());
entity.setEnabled(true);
userRepository.save(entity);
return true;
}
@Override
public List<UserEntity> getUserList() {
return userRepository.findAll();
}
}
测试截图
1、注册用户,因为加入白名单中,所以无需Authorization
2、获取全部用户,请求时需在header中携带Authorization,否则会返回提示用户登录