文章目录
1.简介
1.1Spring Security
Spring Security是一个功能强大且高度可定制的身份验证和访问控制框架。它是用于保护基于Spring的应用程序的实际标准。
Spring Security 相对于Shiro权限框架而已要稍微复杂一点.
1.2.JWT
JWT 英文名是 Json Web Token ,是一种用于通信双方之间传递安全信息的简洁的、URL安全的表述性声明规范,经常用在跨域身份验证。
JWT 以 JSON 对象的形式安全传递信息。因为存在数字签名,因此所传递的信息是安全的。
JWT官网
2.工作流程
3.程序目录结构
4.引入依赖
<!--java web token -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!--spring Security-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
5.核心代码
5.1.Security配置类
- configureAuthentication() 配置身份验证信息
- passwordEncoder() 配置密码的加密使用的算法
- authenticationTokenFilterBean() 构建自定义jwt权限认证过滤器
- configure() 配置资源过滤和注入自定义jwt过滤器
@SuppressWarnings("SpringJavaAutowiringInspection")
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private MyAuthExcetion.MyAuthenticationEntry myAuthenticationEntry;
@Autowired
private MyAuthExcetion.MyAccessDenied myAccessDenied;
@Autowired
private UserDetailsService userDetailsService;
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Autowired
public void configureAuthentication(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
authenticationManagerBuilder
// 设置 UserDetailsService
.userDetailsService(userDetailsService)
// 使用 BCrypt 进行密码的 hash
.passwordEncoder(passwordEncoder());
}
/**
* 装载BCrypt密码编码器
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/**
* 配置自定义jwt权限认证过滤器
*/
@Bean
public JwtAuthenticationTokenFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationTokenFilter();
}
/**
* 设置需要授权认证的资源,不需要权认能访问的资源
*/
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
// we don't need CSRF because our token is invulnerable
.csrf().disable()
.cors().and() // 跨域
.exceptionHandling().authenticationEntryPoint(myAuthenticationEntry).and()
.exceptionHandling().accessDeniedHandler(myAccessDenied).and()
// don't create session
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
//过滤掉不需要权限的地址
.antMatchers(HttpMethod.GET, "/").permitAll()
.antMatchers("/sso/login").permitAll()
// 其它接口都要认证
.anyRequest().authenticated();
//将token验证添加在密码验证前面
httpSecurity.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
// disable page caching
httpSecurity.headers().cacheControl();
}
}
5.2.token生成
token参数配置
在application.yml中配置token参数信息
server:
port: 8016
jwt:
# 加密密钥
secret: iwqjhda8232bjgh432[cicada-smile]
#token有效时长 (单位秒)
expire: 3600
#请求头参数名称
tokenHead: token
生成token工具类
@ConfigurationProperties(prefix = "jwt")
@Component
public class JwtTokenUtils {
private String secret;
private long expire;
private String tokenHead;
//..省略get set方法
//jdk8新增的时钟类
private Clock clock = DefaultClock.INSTANCE;
/**
* 使用用户名作为身份信息生成Token
* @param identityId 用户身份标识
* @param identityId 用户的角色权限信息
*/
public Map getToken(String identityId, List<String> Authorizes) {
Date nowDate = new Date();
//token过期时间
long expireAt=nowDate.getTime() + expire * 1000;
Date expireDate = new Date(expireAt);
Map map=new HashMap<>();
map.put("expireAt",expireAt);
String token= Jwts.builder()
//放入唯一标识,可以是用户名或者Id
.setSubject(identityId)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
//自定义属性 放入用户拥有角色和权限
.claim("roleAuthorizes", Authorizes)
.compact();
map.put("token",token);
return map;
}
/**
* 根据token获取身份信息
*/
public Claims getTokenClaim(String token) {
try {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 判断token是否失效
*/
public boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(clock.now());
}
/**
* 根据token获取username
*/
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
/**
* 根据token获取失效时间
*/
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getTokenClaim(token);
return claimsResolver.apply(claims);
}
}
5.3.JWT资源认证过滤器
@Component
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtils jwtTokenUtils;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
final String token = request.getHeader(jwtTokenUtils.getTokenHead());
//判断当前请求中包含令牌
if (!StringUtils.isEmpty(token)) {
//重token中获取用户的角色权限信息
Claims claims = jwtTokenUtils.getTokenClaim(token);
if (claims != null) {
//如果token未失效 并且 当前上下文权限凭证为null
if (!jwtTokenUtils.isTokenExpired(token) && SecurityContextHolder.getContext().getAuthentication() == null) {
/**
* 这里省略了查询数据库token存不存在,有没有失效这个步骤
* 如果是做单点登录,后登录的人登录时候把上一个人的token状态改为失效,这样就能保证同一时间一个帐号只能有一个人能使用
*/
JwtUserDetail userDetails = new JwtUserDetail(claims);
//把用户权限信息放到上下文中
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
}
filterChain.doFilter(request, response);
}
}
5.4.JwtUserDetail类
/**
* @author Dominick Li
* @CreateTime 2020/4/16 21:28
* @description 重token中获取用户权限信息
**/
public class JwtUserDetail implements UserDetails {
private String username;
private Collection<SimpleGrantedAuthority> authorities;
public JwtUserDetail(Claims claims) {
this.username = claims.getSubject();
//重token中获取权限信息
List<String> roleAuthorizes = claims.get("roleAuthorizes", List.class);
//权限和角色都在roleAuthorizes中,和shiro不同,shiro是权限和角色分开的
authorities = new ArrayList<>(roleAuthorizes.size());
roleAuthorizes.forEach((roleAuthorize) -> {
authorities.add(new SimpleGrantedAuthority(roleAuthorize));
});
}
@Override
public Collection<SimpleGrantedAuthority> getAuthorities() {
return authorities;
}
@Override
@JsonIgnore
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
5.5.自定义认证失败处理类
public class MyAuthExcetion {
@Component
public static class MyAuthenticationEntry implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException authException) throws IOException {
//token认证失败
HashMap map = new HashMap();
map.put("code", 402);
map.put("msg", "toekn is invalid");
response.getWriter().println(map.toString());
}
}
@Component
public static class MyAccessDenied implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
//角色权限认证失败
HashMap map = new HashMap();
map.put("code", 401);
map.put("msg", "accessDenied");
response.getWriter().println(map.toString());
}
}
}
5.6.获取用户权限信息
为方便测试,用户角色权限信息写死了
需要注意的事项,在把角色放入到资源列表中需要在角色前面添加ROLE_
下面是源代码中注入角色时候自动添加ROLE_代码片段
@Service
public class UserDetailsFactory implements UserDetailsService {
/**
* 模拟数据库
* 存放用户对应的角色
*/
HashMap<String, String> userRole = new HashMap<>();
{
userRole.put("test", "USER");
userRole.put("admin", "ADMIN");
}
/**
* 模拟数据库
* 存放角色对应的权限
*/
HashMap<String, List<GrantedAuthority>> roleAuthorize = new HashMap<>();
{
List<GrantedAuthority> uList = new ArrayList<>();
uList.add(new SimpleGrantedAuthority("select"));
uList.add(new SimpleGrantedAuthority("put"));
roleAuthorize.put("USER", uList);
List<GrantedAuthority> aList =new ArrayList<>();
aList.add(new SimpleGrantedAuthority("select"));
aList.add(new SimpleGrantedAuthority("delete"));
roleAuthorize.put("ADMIN", aList);
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//加密过的密码 123456
String password="$2a$10$JA61ycV.otx/d8cJSPAi8Ozbvij5QwDTFxp7jq9Qq3pm0xuZbW6ji";
String roleName=userRole.get(username);
List<GrantedAuthority> authorityList=roleAuthorize.get(roleName);
//security资源注解是把角色和权限一起处理的,shiro是分开处理,需要手动在角色前面添加ROLE_
roleName="ROLE_"+roleName;
authorityList.add(new SimpleGrantedAuthority(roleName));
return User.builder().username(username).password(password).authorities(authorityList).build();
}
}
5.7.业务处理逻辑类
- passwordEncoder.encode() 对密码进行加密
- passwordEncoder.matches() 用原始密码匹配和加密过的匹配进行匹配
@Component
public class UserService {
@Autowired
private JwtTokenUtils jwtTokenUtils;
@Autowired
UserDetailsFactory userDetailService;
@Autowired
PasswordEncoder passwordEncoder;
public Map login(String username, String password) {
Map map = null;
UserDetails user = userDetailService.loadUserByUsername(username);
if (!passwordEncoder.matches(/*原始密码*/password,/*加密过的密码*/user.getPassword())) {
map = new HashMap();
map.put("code", 303);
map.put("msg", "密码错误");
}
List<String> authorites = new ArrayList<>(user.getAuthorities().size());
for (GrantedAuthority authorite : user.getAuthorities()) {
authorites.add(authorite.getAuthority());
}
map = jwtTokenUtils.getToken(username, authorites);
map.put("code",200);
return map;
}
}
5.8.web接口
- @PreAuthorize 资源认证注解
hasRole() 角色拦截
hasAuthority() 权限拦截
@RestController
public class TokenController {
@Autowired
private UserService userService;
/**
* 登录接口
*/
@PostMapping("/sso/login")
public Map login(@RequestParam("username") String username,
@RequestParam("password") String password) {
// 省略数据源校验
return userService.login(username,password);
}
/**
* 需要角色user才能访问的地址
*/
@PreAuthorize("hasRole('USER')")
@GetMapping("/index")
public String info() {
return "index";
}
/**
* 需要角色admin才能访问的地址
*/
@GetMapping("/admin")
@PreAuthorize("hasRole('ADMIN')")
public String admin() {
return "admin";
}
/**
* 需要权限delete 才能删除用户信息
*/
@DeleteMapping("/user/{id}")
@PreAuthorize("hasAuthority('delete')")
public String deleteUser(@PathVariable Integer id) {
System.out.println("删除用户:id=" + id);
return "user";
}
/**
* 需要权限select 才能查看用户信息
*/
@GetMapping("/user/{id}")
@PreAuthorize("hasAuthority('select')")
public String user(@PathVariable Integer id) {
System.out.println("查询用户:id=" + id);
return "user";
}
/**
* 需要权限put 才能修改用户信息
*/
@PutMapping("/user")
@PreAuthorize("hasAuthority('put')")
public String putUser() {
System.out.println("修改用户:id");
return "user";
}
}
6.测试接口
- test帐号角色为: USER
- test帐号权限为: select,put
6.1.获取token信息
输出测试帐号 test和密码 123456,获取token
6.2.访问需要权限认证才能访问接口
- 访问http://localhost:8016/index 接口
- 在请求头中添加token参数,把登陆返回的token参数放入到里面
- 发送GET请求,可以看到结果中能访问
6.3.访问一个test帐号没有的角色资源
- 访问http://localhost:8016/admin 接口
- 因为这个请求"hasRole(‘ADMIN’)" ,所以当前用户访问不了
6.4.输入一个错误的token测试
7.项目配套代码
创作不易,要是觉得我写的对你有点帮助的话,麻烦在gitee上帮我点下 Star
【SpringBoot框架篇】其它文章如下,后续会继续更新。
- 1.搭建第一个springboot项目
- 2.Thymeleaf模板引擎实战
- 3.优化代码,让代码更简洁高效
- 4.集成jta-atomikos实现分布式事务
- 5.分布式锁的实现方式
- 6.docker部署,并挂载配置文件到宿主机上面
- 7.项目发布到生产环境
- 8.搭建自己的spring-boot-starter
- 9.dubbo入门实战
- 10.API接口限流实战
- 11.Spring Data Jpa实战
- 12.使用druid的monitor工具查看sql执行性能
- 13.使用springboot admin对springboot应用进行监控
- 14.mybatis-plus实战
- 15.使用shiro对web应用进行权限认证
- 16.security整合jwt实现对前后端分离的项目进行权限认证
- 17.使用swagger2生成RESTful风格的接口文档
- 18.使用Netty加websocket实现在线聊天功能
- 19.使用spring-session加redis来实现session共享
- 20.自定义@Configuration配置类启用开关
- 21.对springboot框架编译后的jar文件瘦身
- 22.集成RocketMQ实现消息发布和订阅
- 23.集成smart-doc插件零侵入自动生成RESTful格式API文档
- 24.集成FastDFS实现文件的分布式存储
- 25.集成Minio实现文件的私有化对象存储
- 26.集成spring-boot-starter-validation对接口参数校验
- 27.集成mail实现邮件推送带网页样式的消息
- 28.使用JdbcTemplate操作数据库
- 29.Jpa+vue实现单模型的低代码平台
- 30.使用sharding-jdbc实现读写分离和分库分表
- 31.基于分布式锁或xxx-job实现分布式任务调度
- 32.基于注解+redis实现表单防重复提交
- 33.优雅集成i18n实现国际化信息返回
- 34.使用Spring Retry完成任务的重试