SpringSecurity 整合JWT实现无状态登陆
SpringSecurity 整合JWT实现无状态登陆
案例使用SpringBoot作为基础框架快速集成JWT
1.添加启动依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>cn.blogsx</groupId>
<artifactId>springboot_security_jwt</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.1.RELEASE</version>
</parent>
<dependencies>
<!-- web功能起步依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--Spring Security依赖包-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<!-- jwt -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
<!-- mybatis依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>1.3.2</version>
</dependency>
<!-- druid数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
<!-- mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
</project>
2.添加配置文件并配置基本信息
server.port=8080
# 数据库连接相关配置
spring.datasource.url=jdbc:mysql:///springsecurity?characterEncoding=utf8&useSSL=true
spring.datasource.driverClassName=com.mysql.jdbc.Driver
spring.datasource.username=root
spring.datasource.password=root
# MyBatis注解形式扫描实体类路径
mybatis.type-aliases-package=cn.blogsx.entity
# MyBatis XML形式配置文件路径
mybatis.config-locations=classpath:mybatis/mybatis-config.xml
mybatis.mapper-locations=classpath:mybatis/mapper/*.xml
# 配置Jwt密钥
jwt.secret=Alex
3.创建数据库表信息
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for role
-- ----------------------------
DROP TABLE IF EXISTS `role`;
CREATE TABLE `role` (
`id` int(11) NOT NULL,
`name` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
`nameZh` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of role
-- ----------------------------
INSERT INTO `role` VALUES (1, 'ROLE_dba', '数据库管理员');
INSERT INTO `role` VALUES (2, 'ROLE_admin', '系统管理员');
INSERT INTO `role` VALUES (3, 'ROLE_user', '用户');
-- ----------------------------
-- Table structure for user
-- ----------------------------
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL,
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_unicode_ci NULL DEFAULT NULL,
`enabled` tinyint(1) NULL DEFAULT NULL,
`locked` tinyint(1) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Dynamic;
-- ----------------------------
-- Records of user
-- ----------------------------
INSERT INTO `user` VALUES (1, 'root', '$2a$10$8XXMNg8WQ8YlSIGGcgnaw./zrf2k6klkqXs0ezawj43VN7uh/m8Wu', 1, 0);
INSERT INTO `user` VALUES (2, 'admin', '$2a$10$8XXMNg8WQ8YlSIGGcgnaw./zrf2k6klkqXs0ezawj43VN7uh/m8Wu', 1, 0);
INSERT INTO `user` VALUES (3, 'alex', '$2a$10$8XXMNg8WQ8YlSIGGcgnaw./zrf2k6klkqXs0ezawj43VN7uh/m8Wu', 1, 0);
-- ----------------------------
-- Table structure for user_role
-- ----------------------------
DROP TABLE IF EXISTS `user_role`;
CREATE TABLE `user_role` (
`id` int(11) NOT NULL,
`uid` int(11) NULL DEFAULT NULL,
`rid` int(11) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = MyISAM CHARACTER SET = utf8 COLLATE = utf8_unicode_ci ROW_FORMAT = Fixed;
-- ----------------------------
-- Records of user_role
-- ----------------------------
INSERT INTO `user_role` VALUES (1, 1, 1);
INSERT INTO `user_role` VALUES (2, 1, 2);
INSERT INTO `user_role` VALUES (3, 2, 2);
INSERT INTO `user_role` VALUES (4, 3, 3);
SET FOREIGN_KEY_CHECKS = 1;
4.新建测试接口
接口
@RestController
public class UserController {
@RequestMapping("/hello")
public Object hello() {
String str = "hello";
return str;
}
//接口调用前判断是否又admin角色
@PreAuthorize("hasRole('admin')") //此处使用注解实现方法级的安全,也可以在SecurityConfig中统一配置
@RequestMapping("/admin/hello")
public Object adminHello() {
String str = "/admin/hello";
return str;
}
}
Bean
package cn.blogsx.entity;
public class Role {
private Integer id;
private String name;
private String nameZh;
//省略getter和setter及构造方法
}
//user对象需要实现UserDetails接口才能在UserDetailsServiceImpl中的loadUserByUsername方法使用
ublic class User implements UserDetails {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Boolean locked;
private List<Role> roles;
public User() {
}
public User(Integer id, String username, String password, Boolean enabled, Boolean locked, List<Role> roles) {
this.id = id;
this.username = username;
this.password = password;
this.enabled = enabled;
this.locked = locked;
this.roles = roles;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<SimpleGrantedAuthority> authorities = new ArrayList<>();
for (Role role:roles){
authorities.add(new SimpleGrantedAuthority(role.getName()));
}
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !locked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return enabled;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public List<Role> getRoles() {
return roles;
}
public void setRoles(List<Role> roles) {
this.roles = roles;
}
}
5.创建SecurityConfig配置,配置接口安全策略
@Configuration
//使用 @PreAuthorize("hasRole('admin')") 方法级安全注解时必须使用该注解声明才能使用
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private JwtConfig jwtConfig;
@Autowired
UserDetailsServiceImpl userServiceImpl;
/**
* 配置SpringSecurity 加密方式
* @return 加密对象
*/
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//使用数据库查询用户作用认证数据源
auth.userDetailsService(userServiceImpl);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
//由于下文的jwt过滤器不使用spring来管理,故jwt所需配置需要在注册时设置配置文件值
JwtLoginFilter jwtLoginFilter = new JwtLoginFilter("/login", authenticationManager());
jwtLoginFilter.setSecret(jwtConfig.getSecret());
JwtFilter jwtFilter = new JwtFilter(jwtConfig.getSecret());
http.authorizeRequests()
//配置统一拦截路径,也可使用 @PreAuthorize("hasRole('admin')")类似注解灵活配置
// .antMatchers("/hello")
// .hasRole("user")
// .antMatchers("/admin")
// .hasRole("admin")
//配置登陆接口
.antMatchers(HttpMethod.POST, "/login")
.permitAll()
.anyRequest().authenticated()
.and()
//配置jwt过滤器
.addFilterBefore(jwtLoginFilter, UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class)
.csrf().disable();
}
}
jwtconfig
@Configuration
public class JwtConfig {
@Value("${jwt.secret}") //使用该注解一定要是被spring管理的类才能注入值,过滤器或监听器无法使用(因为spring中的类加载顺序是:listener->filter->servlet)
private String secret; //jwt密钥
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
}
6.配置JWT过滤器
jwt登陆过滤器(用于在登陆时颁发toekn)
public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
private String secret; //jwt密钥
public String getSecret() {
return secret;
}
public void setSecret(String secret) {
this.secret = secret;
}
public JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
setAuthenticationManager(authenticationManager);
}
/**
* 配置Jwt登陆拦截器
* @param req
* @param httpServletResponse
* @return
* @throws AuthenticationException
* @throws IOException
*/
@Override
public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse httpServletResponse) throws AuthenticationException, IOException {
User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
}
/**
* 登陆成功后返回Token
* @param request
* @param resp
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@Override
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException {
Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();//获取登录用户的角色
StringBuffer sb = new StringBuffer();
for (GrantedAuthority authority : authorities) {
sb.append(authority.getAuthority()).append(",");
}
String jwt = Jwts.builder()
.claim("authorities", sb)
.setSubject(authResult.getName())
.setExpiration(new Date(System.currentTimeMillis() + 60 * 60 * 1000))
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
Map<String, String> map = new HashMap<>();
map.put("token", jwt);
map.put("msg", "登录成功");
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
/**
* 登陆失败,返回json提示信息
* @param req
* @param resp
* @param failed
* @throws IOException
* @throws ServletException
*/
@Override
protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException {
Map<String, String> map = new HashMap<>();
map.put("msg", "登录失败");
resp.setContentType("application/json;charset=utf-8");
PrintWriter out = resp.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
}
}
jwt过滤器(用于每次请求过滤校验token的合法性等)
/**
* 配置登陆后每次拦截jwt检验token合法性拦截器,无需查询数据库
*/
public class JwtFilter extends GenericFilterBean {
private String secret;
public JwtFilter(String secret) {
this.secret = secret;
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) servletRequest;
String jwtToken = req.getHeader("authorization");
Jws<Claims> jws=null;
try {
jws= Jwts.parser().setSigningKey(secret)
.parseClaimsJws(jwtToken.replace("Bearer", ""));
}catch (ExpiredJwtException e) {
//Token已过期,返回提示信息
Map<String, String> map = new HashMap<>();
map.put("msg", "Token已过期,请重新登陆");
servletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = servletResponse.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
return;
} catch (SignatureException e){
//Token签名异常,返回提示信息
Map<String, String> map = new HashMap<>();
map.put("msg", "Token签名异常");
servletResponse.setContentType("application/json;charset=utf-8");
PrintWriter out = servletResponse.getWriter();
out.write(new ObjectMapper().writeValueAsString(map));
out.flush();
out.close();
return;
}
Claims claims = jws.getBody();
String username = claims.getSubject();
List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities);
SecurityContextHolder.getContext().setAuthentication(token);
filterChain.doFilter(servletRequest,servletResponse);
}
}