记录学习Spring boot 整合Spring Security Jwt
学习参考 – 慢慢的干货
https://shimo.im/docs/OnZDwoxFFL8bnP1c/read
首先创建Spring Boot项目以及导入依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.22</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.3.1.tmp</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.5.7</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
application.yml配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/securitytext?serverTimezone=GMT&characterEncoding=utf-8
username: root
password: 1233
dbcp2:
test-on-borrow: true
validation-query: SELECT 1
hikari:
max-lifetime: 30000
servlet:
multipart:
max-file-size: 1024MB
max-request-size: 1024MB
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
server:
port: 8030
xiaojiang:
jwt:
secret: f4e2e52034348f86b67cde581c0f9eb5
expire: 604800
header: Authorization
创建数据库
DROP TABLE IF EXISTS `sys_user`;
CREATE TABLE `sys_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户id',
`username` varchar(64) DEFAULT NULL COMMENT '用户名',
`password` varchar(64) DEFAULT NULL COMMENT '用户密码',
`avatar` varchar(255) DEFAULT NULL COMMENT '用户头像',
`email` varchar(64) DEFAULT NULL COMMENT '用户邮箱',
`city` varchar(64) DEFAULT NULL COMMENT '城市',
`created_time` datetime DEFAULT NULL COMMENT '用户创建时间',
`updated_time` datetime DEFAULT NULL COMMENT '用户修改时间',
`last_login_time` datetime DEFAULT NULL COMMENT '用户上次登录时间',
`statu` int(5) NOT NULL COMMENT '用户状态 1可用|0不可用',
PRIMARY KEY (`id`),
UNIQUE KEY `UK_USERNAME` (`username`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `sys_user` VALUES ('1', 'admin', '$2a$10$R7zegeWzOXPw871CmNuJ6upC0v8D373GuLuTw8jn6NET4BkPRZfgK', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', '123@qq.com', '广州', '2021-01-12 22:13:53', '2021-01-16 16:57:32', '2020-12-30 08:38:37', '1');
INSERT INTO `sys_user` VALUES ('2', 'test', '$2a$10$0ilP4ZD1kLugYwLCs4pmb.ZT9cFqzOZTNaMiHxrBnVIQUGUwEvBIO', 'https://image-1300566513.cos.ap-guangzhou.myqcloud.com/upload/images/5a9f48118166308daba8b6da7e466aab.jpg', 'test@qq.com', null, '2021-01-30 08:20:22', '2021-01-30 08:55:57', null, '1');
DROP TABLE IF EXISTS `sys_role`;
CREATE TABLE `sys_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '角色id',
`name` varchar(64) NOT NULL COMMENT '角色名称',
`code` varchar(64) NOT NULL COMMENT '角色',
`created_time` datetime DEFAULT NULL COMMENT'角色创建时间',
`updated_time` datetime DEFAULT NULL COMMENT '角色修改时间',
`statu` int(5) NOT NULL COMMENT '角色状态 1可用|0不可用',
PRIMARY KEY (`id`),
UNIQUE KEY `name` (`name`) USING BTREE,
UNIQUE KEY `code` (`code`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;
INSERT INTO `sys_role` VALUES ('1', '超级管理员', 'admin', '2021-01-16 13:29:03', '2021-01-17 15:50:45', '1');
INSERT INTO `sys_role` VALUES ('2', '普通用户', 'normal', '2021-01-04 10:09:14', '2021-01-30 08:19:52', '1');
DROP TABLE IF EXISTS `sys_user_role`;
CREATE TABLE `sys_user_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`user_id` bigint(20) NOT NULL COMMENT '用户id',
`role_id` bigint(20) NOT NULL COMMENT '角色id',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;
INSERT INTO `sys_user_role` VALUES ('1', '1', '1');
INSERT INTO `sys_user_role` VALUES ('2', '1', '2');
INSERT INTO `sys_user_role` VALUES ('3', '2', '2');

使用mybatis-plus代码生成器
public class CodeGenerator {
public static String scanner(String tip) {
Scanner scanner = new Scanner(System.in);
StringBuilder help = new StringBuilder();
help.append("请输入" + tip + ":");
System.out.println(help.toString());
if (scanner.hasNext()) {
String ipt = scanner.next();
if (StringUtils.isNotEmpty(ipt)) {
return ipt;
}
}
throw new MybatisPlusException("请输入正确的" + tip + "!");
}
public static void main(String[] args) {
AutoGenerator mpg = new AutoGenerator();
GlobalConfig gc = new GlobalConfig();
String projectPath = System.getProperty("user.dir");
gc.setOutputDir(projectPath + "/src/main/java");
gc.setAuthor("Mr.jiang");
gc.setOpen(false);
gc.setServiceName("%sService");
mpg.setGlobalConfig(gc);
DataSourceConfig dsc = new DataSourceConfig();
dsc.setDbType(DbType.MYSQL);
dsc.setUrl("jdbc:mysql://localhost:3306/securitytext?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai");
dsc.setDriverName("com.mysql.cj.jdbc.Driver");
dsc.setUsername("root");
dsc.setPassword("1233");
mpg.setDataSource(dsc);
PackageConfig pc = new PackageConfig();
pc.setModuleName(null);
pc.setParent("com.jsp");
mpg.setPackageInfo(pc);
StrategyConfig strategy = new StrategyConfig();
strategy.setNaming(NamingStrategy.underline_to_camel);
strategy.setColumnNaming(NamingStrategy.underline_to_camel);
strategy.setEntityLombokModel(true);
strategy.setRestControllerStyle(true);
strategy.setInclude(scanner("表名,多个英文逗号分割").split(","));
strategy.setControllerMappingHyphenStyle(true);
strategy.setTablePrefix("sys_");
mpg.setStrategy(strategy);
mpg.execute();
}
}
创建mybatis-plus配置类
@Configuration
public class MybatisPlusConfig {
@Bean
public PaginationInterceptor paginationInterceptor() {
PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
return paginationInterceptor;
}
}
@Component
public class MyMetaObjectConfig implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}
这里是为了entiy类中的字段
@TableField(fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "GMT+8")
private LocalDateTime createdTime;
@TableField(fill = FieldFill.INSERT)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "GMT+8")
private LocalDateTime updatedTime;
@TableField(fill = FieldFill.INSERT_UPDATE)
@JsonFormat(pattern = "yyyy-MM-dd HH:mm", timezone = "GMT+8")
private LocalDateTime lastLoginTime;
Result结果集
@Data
public class Result implements Serializable {
private int code;
private String message;
private Object data;
Result(int code,String message,Object data) {
this.code = code;
this.message = message;
this.data = data;
}
Result(int code,String message) {
this.code = code;
this.message = message;
}
}
public enum ResultCode {
SUCCESS(200),
FAIL(400),
UNAUTHORIZED(401),
NOT_FOUND(404),
FORBIDDEN(403),
INTERNAL_SERVER_ERROR(500);
public int code;
ResultCode(int code) {
this.code = code;
}
}
public class ResultFactory {
public static Result successful(Object data) {
return buildResult(ResultCode.SUCCESS, "成功", data);
}
public static Result fail(String message) {
return buildResult(ResultCode.FAIL, message, null);
}
public static Result buildResult(int ResultCode, String message, Object data) {
return new Result(ResultCode, message, data);
}
public static Result buildResult(ResultCode ResultCode, String message, Object data) {
return new Result(ResultCode.code, message, data);
}
public static Result buildResult(ResultCode ResultCode, String message) {
return new Result(ResultCode.code, message);
}
}
** 重点 Spring Security 配置类
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private LoginFailureHandler loginFailureHandler;
@Autowired
private LoginSuccessHandler loginSuccessHandler;
@Autowired
private UserDetailsServiceImpl userDetailsService;
@Autowired
private JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
private JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
private JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
@Bean
JWTAuthenticationFilter jwtAuthenticationFilter() throws Exception {
return new JWTAuthenticationFilter(authenticationManager());
}
@Bean
BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
public static final String[] URL_WHITELIST = {
"/favicon.ico",
"/login",
"/logout",
};
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.formLogin()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
.and()
.addFilter(jwtAuthenticationFilter());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
重写UserDetails
public class MyUserDetails implements UserDetails {
private Integer id;
private String password;
private final String username;
private final Collection<? extends GrantedAuthority> authorities;
private final boolean accountNonExpired;
private final boolean accountNonLocked;
private final boolean credentialsNonExpired;
private final boolean enabled;
public MyUserDetails(Integer id, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this(id, username, password, true, true, true, true, authorities);
}
public MyUserDetails(Integer id, String username, String password, boolean enabled, boolean accountNonExpired,
boolean credentialsNonExpired, boolean accountNonLocked,
Collection<? extends GrantedAuthority> authorities) {
Assert.isTrue(username != null && !"".equals(username) && password != null,
"Cannot pass null or empty values to constructor");
this.id = id;
this.username = username;
this.password = password;
this.enabled = enabled;
this.accountNonExpired = accountNonExpired;
this.credentialsNonExpired = credentialsNonExpired;
this.accountNonLocked = accountNonLocked;
this.authorities = authorities;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return this.authorities;
}
@Override
public String getPassword() {
return this.password;
}
@Override
public String getUsername() {
return this.username;
}
@Override
public boolean isAccountNonExpired() {
return this.accountNonExpired;
}
@Override
public boolean isAccountNonLocked() {
return this.accountNonLocked;
}
@Override
public boolean isCredentialsNonExpired() {
return this.credentialsNonExpired;
}
@Override
public boolean isEnabled() {
return this.enabled;
}
}
重写UserDetailsService
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
User user = userMapper.selectOne(wrapper);
if (user == null) {
throw new UsernameNotFoundException("用户名或密码不正确!");
}
List<GrantedAuthority> authorityList = getUserAuthority(user.getId());
return new MyUserDetails(user.getId(), user.getUsername(), user.getPassword(), authorityList);
}
public List<GrantedAuthority> getUserAuthority(Integer id) {
return AuthorityUtils.commaSeparatedStringToAuthorityList(
userService.getUserAuthorityInfo(id)
);
}
}
public interface UserRoleMapper extends BaseMapper<UserRole> {
@Select("SELECT user_id,role_id,code,name FROM sys_user_role JOIN sys_role ON sys_user_role.role_id = sys_role.id WHERE user_id = #{uid};")
List<RolesVo> selectRoles(Integer uid);
}
VO类
RolesVo
@Data
@AllArgsConstructor
@NoArgsConstructor
public class RolesVo {
String code;
String name;
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private UserMapper userMapper;
@Override
public String getUserAuthorityInfo(Integer id) {
List<RolesVo> roles = userRoleMapper.selectRoles(id);
String roleNames = roles.stream().map(role -> "ROLE_" + role.getCode()).collect(Collectors.joining(","));
String authority = roleNames.concat(",").concat("sys:user:list");
System.out.println("authority->" + authority);
return authority;
}
@Override
public User getByUsername(String username) {
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("username", username);
User user = userMapper.selectOne(wrapper);
return user;
}
}
这里还有菜单管理的 String authority = roleNames.concat(",").concat("sys:user:list"); 以后还会补上的

LoginSuccessHandler
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {
@Autowired
private JwtUtil jwtUtil;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
String token = jwtUtil.generateToken(authentication.getName());
response.setHeader(jwtUtil.getHeader(), token);
Result result = ResultFactory.successful("");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
LoginFailureHandler
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
Result result = ResultFactory.fail("Bad credentials".equals(exception.getMessage()) ? "用户名或密码不正确" : exception.getMessage());
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
JwtLogoutSuccessHandler
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
private JwtUtil jwtUtil;
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
response.setHeader(jwtUtil.getHeader(), "");
Result result = ResultFactory.successful("");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
JwtAuthenticationEntryPoint
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest httpServletRequest, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ServletOutputStream outputStream = response.getOutputStream();
Result result = ResultFactory.buildResult(ResultCode.UNAUTHORIZED, "请先登录!");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
JwtAccessDeniedHandler
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest httpServletRequest, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
ServletOutputStream outputStream = response.getOutputStream();
Result result = ResultFactory.buildResult(ResultCode.FORBIDDEN,"权限不足");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();
}
}
jwt相关配置
JwtUtil
@Data
@Component
@ConfigurationProperties(prefix = "xiaojiang.jwt")
public class JwtUtil {
private long expire;
private String secret;
private String header;
public String generateToken(String username) {
Date exireDate = new Date(new Date().getTime() + 1000 * expire);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(username)
.setIssuedAt(new Date())
.setExpiration(exireDate)
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public Claims getClaimByToken(String jwt) {
try {
return Jwts.parser()
.setSigningKey(secret)
.parseClaimsJws(jwt)
.getBody();
} catch (Exception e) {
return null;
}
}
public boolean isTokenExpired(Claims claims) {
return claims.getExpiration().before(new Date());
}
}
JWTAuthenticationFilter
public class JWTAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
private UserService userService;
@Autowired
private JwtUtil jwtUtil;
@Autowired
private UserDetailsServiceImpl userDetailsService;
public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
String jwt = request.getHeader(jwtUtil.getHeader());
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
Claims claim = jwtUtil.getClaimByToken(jwt);
if (claim == null) {
throw new JwtException("token异常");
}
if (jwtUtil.isTokenExpired(claim)) {
throw new JwtException("token已过期");
}
String username = claim.getSubject();
User user = userService.getByUsername(username);
List<GrantedAuthority> userAuthority = userDetailsService.getUserAuthority(user.getId());
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, userAuthority);
System.out.println("token->" + token);
SecurityContextHolder.getContext().setAuthentication(token);
chain.doFilter(request, response);
}
}
ok大功告成 使用postman试试效果
admin用户拥有admin和normal角色,test用户拥有normal角色
在Controller中分配接口权限
@RestController
@RequestMapping("/user")
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class UserController {
@Autowired
private UserService userService;
@PreAuthorize("hasRole('admin')")
@GetMapping("/admin")
public String hello() {
return "hello admin";
}
@PreAuthorize("hasRole('normal')")
@GetMapping("/test")
public String test() {
return "hello test";
}
@PreAuthorize("hasRole('admin')")
@GetMapping("/list")
public Result list() {
return ResultFactory.successful(userService.list());
}
}
首先使用admin用户登录

在Header中有Authorization,也就是我们需要的token
 
接着使用admin用户的token去访问其他接口


admin用户都可以访问
接着使用test用户登录
  
使用test用户去访问其他接口


因为test用户只有normal角色,所以只能访问test接口,admin接口权限不足
|