写在前面 本文将在第三篇《整合Redis实现数据缓存》的基础上整合SpringSecurity和JWT,实现认证与授权这一功能。
使用的框架简介 SpringSecurity SpringSecurity是一个强大的可高度定制的认证与授权框架,对于Spring应用来说它是一套Web安全标准。SpringSecurity注重于为Java应用提供认证和授权功能,像所有的Spring项目一样,它对自定义需求具有强大的扩展性。关于SpringSecurity的学习,可以参考笔者的其他文章。
JWT JWT简介 JWT是JSON WEB TOKEN的缩写,它是基于RFC 7519 标准定义的一种可以安全传输的的JSON对象,由于使用了数字签名,所以是可信任和安全的。
JWT组成 JWT由三部分组成:header、playload和signature,JWT token的格式为header.payload.signature
。
其中header中用于存放签名的生成算法:
payload中用于存放用户名、token的生成时间和过期时间:
1 {"sub":"admin","created":1489079981393,"exp":1489684781}
signature为以header和payload生成的签名,一旦header和payload被篡改,那么验证将失败:
1 2 //secret为加密算法的密钥 String signature = HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
JWT示例 如下是一个JWT的字符串:
1 eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImNyZWF0ZWQiOjE1NTY3NzkxMjUzMDksImV4cCI6MTU1NzM4MzkyNX0.d-iki0193X0bBOETf2UN3r3PotNIEAV7mzIxxeI5IxFyzzkOZxS0PGfF_SK6wxCv2K8S0cZjMkv6b5bCqc0VBw
开发者可以点击 这里 获取到解析结果:
JWT实现认证和授权的原理 第一步,用户调用登录接口,登录成功后获取到JWT的token;
第二步,用户后续每次调用接口时,都会在http的header中添加一个名为Authorization的头,值为JWT的token;
第三步,后台程序通过对Authorization头中信息的解码及数字签名,校验来获取其中的用户信息,进而实现认证和授权。
Hutool是一个丰富的Java开源工具包,可以帮助我们简化每一行代码,减少每一个方法。
项目使用到的表说明 由于此处采用的是RBAC(基于角色的权限控制)模型,一般会涉及到五张表,同时外加针对某个用户的特定角色,因此会涉及到六张表,分别如下所示: (1)ums_admin
:后台用户表; (2)ums_role
:后台用户角色表; (3)ums_permission
:后台用户权限表; (4)ums_admin_role_relation
:后台用户和角色关系表,用户与角色是多对多关系; (5) ums_role_permission_relation
:后台用户角色和权限关系表,角色与权限是多对多关系; (6)ums_admin_permission_relation
:后台用户和权限关系表(除角色中定义的权限以外的加减权限),加权限是指用户比角色多出的权限,减权限是指用户比角色少的权限。
整合SpringSecurity及JWT 第一步 ,复制一份shop-redis
源码,将其名字修改为shop-springsecurity-jwt
,然后对应包和文件中的信息也记得修改,本篇后续所有操作均在shop-springsecurity-jwt
这一Module中进行。注意复制之后需要重新执行一下Generator类,以覆盖之前项目的自动生成文件。
第二步 ,在shop-springsecurity-jwt
的POM文件中新增如下依赖:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <!--SpringSecurity依赖--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <!--Hutool Java工具包--> <dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>4.5.7</version> </dependency> <!--JWT(Json Web Token)登录支持--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
第三步 ,往application.yml配置文件中在根节点下添加jwt自定义key的相关配置信息:
1 2 3 4 5 6 # 自定义jwt的key jwt: tokenHeader: Authorization #JWT存储的请求头 secret: mySecret #JWT加解密使用的密钥 expiration: 604800 #JWT的超期限时间(60*60*24) tokenHead: Bearer #JWT负载中拿到开头
第四步 ,在com.kenbings.shop.shopspringsecurityjwt.common
包内新建一个名为utils的包,并在utils包内定义一个名为JwtTokenUtil的工具类,该类用于生成和解析JWT token以获取对应信息:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 /** * JwtToken生成的工具类 * JWT token的格式:header.payload.signature * header的格式(算法、token的类型): * {"alg": "HS512","typ": "JWT"} * payload的格式(用户名、创建时间、生成时间): * {"sub":"kenbings","created":1489079981393,"exp":1489684781} * signature的生成算法: * HMACSHA512(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret) */ @Component public class JwtTokenUtil { private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class); private static final String CLAIM_KEY_USERNAME = "sub"; private static final String CLAIM_KEY_CREATED = "created"; @Value("${jwt.secret}") private String secret; @Value("${jwt.expiration}") private Long expiration; /** * 生成JWT的token */ private String generateToken(Map<String, Object> claims) { return Jwts.builder() .setClaims(claims) .setExpiration(generateExpirationDate()) .signWith(SignatureAlgorithm.HS512, secret) .compact(); } /** * 从token中获取JWT中的负载 */ private Claims getClaimsFromToken(String token) { Claims claims = null; try { claims = Jwts.parser() .setSigningKey(secret) .parseClaimsJws(token) .getBody(); } catch (Exception e) { LOGGER.info("JWT格式验证失败:{}",token); } return claims; } /** * 生成token的过期时间 */ private Date generateExpirationDate() { return new Date(System.currentTimeMillis() + expiration * 1000); } /** * 从token中获取登录用户名 */ public String getUserNameFromToken(String token) { String username; try { Claims claims = getClaimsFromToken(token); username = claims.getSubject(); } catch (Exception e) { username = null; } return username; } /** * 验证token是否还有效 * * @param token 客户端传入的token * @param userDetails 从数据库中查询出来的用户信息 */ public boolean validateToken(String token, UserDetails userDetails) { String username = getUserNameFromToken(token); return username.equals(userDetails.getUsername()) && !isTokenExpired(token); } /** * 判断token是否已经失效 */ private boolean isTokenExpired(String token) { Date expiredDate = getExpiredDateFromToken(token); return expiredDate.before(new Date()); } /** * 从token中获取过期时间 */ private Date getExpiredDateFromToken(String token) { Claims claims = getClaimsFromToken(token); return claims.getExpiration(); } /** * 根据用户信息生成token */ public String generateToken(UserDetails userDetails) { Map<String, Object> claims = new HashMap<>(); claims.put(CLAIM_KEY_USERNAME, userDetails.getUsername()); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } /** * 判断token是否可以被刷新 */ public boolean canRefresh(String token) { return !isTokenExpired(token); } /** * 刷新token */ public String refreshToken(String token) { Claims claims = getClaimsFromToken(token); claims.put(CLAIM_KEY_CREATED, new Date()); return generateToken(claims); } }
简单说一下其中比较重要的三个方法的作用: (1)generateToken(UserDetails userDetails)
方法,传入的是一个UserDetails对象,这是SpringSecurity提供的对象,其实就是用户登录成功后信息的一个封装对象; (2)getUserNameFromToken(String token)
方法,从token中获取用户姓名,诸如此类的方法还有很多,这里就不再说明; (3)validateToken(String token, UserDetails userDetails)
方法,用于判断token是否有效,其中token是客户端传入的,而userDetails则是从数据库中查询出来的用户信息。
第五步 ,在com.kenbings.shop.shopspringsecurityjwt
包内新建一个名为component的包,并在component包内定义一个名为RestfulAccessDeniedHandler
的类,该类定义当访问接口没有权限时,应当返回的结果,注意它需要实现AccessDeniedHandler
接口,并重写其中的handle方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 /** * 当访问接口没有权限时,返回自定义的结果 */ @Component public class RestfulAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(JSONUtil.parse(CommonResult.forbidden(e.getMessage()))); response.getWriter().flush(); } }
第六步 ,在component包内定义一个名为RestAuthenticationEntryPoint
的类,该类定义当未登录或者token失效访问接口时,应当返回的结果,注意它需要实现AuthenticationEntryPoint
接口,并重写其中的commence方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 /** * 当未登录或者token失效访问接口时,返回自定义的结果 */ @Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setCharacterEncoding("UTF-8"); response.setContentType("application/json"); response.getWriter().println(JSONUtil.parse(CommonResult.unauthorized(e.getMessage()))); response.getWriter().flush(); } }
第七步 ,修改generatorConfig.xml
配置文件中的数据表信息,以生成前面所述六张表的对应信息。
第八步 ,在com.kenbings.shop.shopspringsecurityjwt
包内新建一个名为dto的包,并在dto包内定义一个名为AdminUserDetails
的类,该类定义SpringSecurity登录时需要使用到的UserDetails,注意它需要实现UserDetails
接口,并重写其中对应的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 /** * SpringSecurity登录时需要使用到的UserDetails */ public class AdminUserDetails implements UserDetails { private UmsAdmin umsAdmin; private List<UmsPermission> permissionList; public AdminUserDetails(UmsAdmin umsAdmin,List<UmsPermission> permissionList){ this.umsAdmin = umsAdmin; this.permissionList = permissionList; } /** * 返回当前用户的权限 */ @Override public Collection<? extends GrantedAuthority> getAuthorities() { return permissionList.stream().filter(permission -> permission.getValue()!=null) .map(permission -> new SimpleGrantedAuthority(permission.getValue())) .collect(Collectors.toList()); } @Override public String getPassword() { return umsAdmin.getPassword(); } @Override public String getUsername() { return umsAdmin.getUsername(); } @Override public boolean isAccountNonExpired() { return true; } @Override public boolean isAccountNonLocked() { return true; } @Override public boolean isCredentialsNonExpired() { return true; } @Override public boolean isEnabled() { return umsAdmin.getStatus().equals(1); } }
请注意,正常来说这里面方法的逻辑都需要在数据库中定义对应的字段信息,然后根据业务逻辑来进行判断,而不是像这里直接固定化。这个类不需要交由Spring去管理,因此不需要添加@Component
注解。
第九步 ,在component包内定义一个名为JwtAuthenticationTokenFilter
的类,这是JWT登录授权过滤器,注意它需要继承OncePerRequestFilter
类,并重写其中的doFilterInternal方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 /** * JWT登录授权过滤器 */ public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private static final Logger LOGGER = LoggerFactory.getLogger(JwtAuthenticationTokenFilter.class); @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String authHeader = request.getHeader(this.tokenHeader); if (authHeader != null && authHeader.startsWith(this.tokenHead)) { //获取"Bearer "之后的部分 String authToken = authHeader.substring(this.tokenHead.length()); String username = jwtTokenUtil.getUserNameFromToken(authToken); LOGGER.info("检查用户:{}", username); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtTokenUtil.validateToken(authToken, userDetails)) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); LOGGER.info("当前认证用户:{}", username); SecurityContextHolder.getContext().setAuthentication(authentication); } } } chain.doFilter(request, response); } }
这是在校验用户名和密码之前添加的过滤器,如果请求中包含jwt且token有效,那么会从中用户名,然后调用SpringSecurity的相关API来进行登录操作。
第十步 ,在service包内定义一个名为UmsAdminService
的接口,用于定义后台管理员相关的接口方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /** * 后台管理员Service */ public interface UmsAdminService { /** * 根据用户名查询后台管理员 */ UmsAdmin getAdminByUsername(String username); /** * 后台管理员注册 */ UmsAdmin register(UmsAdmin umsAdminParam); /** * 后台管理员登录 * @param username 用户名 * @param password 密码 * @return 生成的JWT的token */ String login(String username, String password); /** * 获取指定后台管理员的所有权限(包括角色权限和+-权限) */ List<UmsPermission> getPermissionList(Long adminId); }
第十一步 ,在impl包内定义一个名为UmsAdminServiceImpl
的类,这个类需要实现UmsAdminService
接口,并重写其中的方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 /** * 后台管理员Service实现类 */ @Service public class UmsAdminServiceImpl implements UmsAdminService { private static final Logger LOGGER = LoggerFactory.getLogger(UmsAdminServiceImpl.class); @Value("${jwt.tokenHead}") private String tokenHead; @Autowired private UserDetailsService userDetailsService; @Autowired private JwtTokenUtil jwtTokenUtil; @Autowired private PasswordEncoder passwordEncoder; @Autowired private UmsAdminMapper adminMapper; @Autowired private UmsAdminRoleRelationDao adminRoleRelationDao; @Override public UmsAdmin getAdminByUsername(String username) { UmsAdminExample example = new UmsAdminExample(); example.createCriteria().andUsernameEqualTo(username); List<UmsAdmin> adminLists = adminMapper.selectByExample(example); if(adminLists !=null && !adminLists.isEmpty()){ return adminLists.get(0); } return null; } @Override public UmsAdmin register(UmsAdmin umsAdminParam) { //查询是否有相同用户名的用户 UmsAdminExample example = new UmsAdminExample(); example.createCriteria().andUsernameEqualTo(umsAdminParam.getUsername()); List<UmsAdmin> umsAdminList = adminMapper.selectByExample(example); if (!umsAdminList.isEmpty()) { return null; } UmsAdmin umsAdmin = new UmsAdmin(); BeanUtils.copyProperties(umsAdminParam, umsAdmin); umsAdmin.setCreateTime(new Date()); umsAdmin.setStatus(1); //将密码进行加密操作 String encodePassword = passwordEncoder.encode(umsAdmin.getPassword()); umsAdmin.setPassword(encodePassword); adminMapper.insert(umsAdmin); return umsAdmin; } @Override public String login(String username, String password) { String token = null; try { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if(userDetails == null){ throw new BadCredentialsException("不存在对应的用户信息"); } if (!passwordEncoder.matches(password, userDetails.getPassword())) { throw new BadCredentialsException("密码不正确"); } UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); token = jwtTokenUtil.generateToken(userDetails); } catch (AuthenticationException e) { LOGGER.warn("登录异常:{}", e.getMessage()); } return token; } @Override public List<UmsPermission> getPermissionList(Long adminId) { return adminRoleRelationDao.getPermissionList(adminId); } }
可以看到在后面我们定义了getPermissionList(Long adminId)
方法,该方法用于根据adminId从数据库中查询用户所有的权限,包括角色权限以及个人独有的权限。
第十二步 ,在com.kenbings.shop.shopspringsecurityjwt
包内新建一个名为dao的包,并在dao包内定义一个名为UmsAdminRoleRelationDao
的接口,这是一个自定义的dao,用于查询后台用户与角色:
1 2 3 4 5 6 7 8 9 /** * 用于查询后台用户与角色的自定义DAO */ public interface UmsAdminRoleRelationDao { /** * 获取用户所有权限(包括+-权限) */ List<UmsPermission> getPermissionList(@Param("adminId") Long adminId); }
第十三步 ,在resources目录下新建一个名为mapper的目录,并在mapper目录下定义一个名为UmsAdminRoleRelationDao.xml
的XML文件,这个其实就是之前UmsAdminRoleRelationDao接口对应的XML文件:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 <?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.kenbings.shop.shopspringsecurityjwt.dao.UmsAdminRoleRelationDao"> <select id="getPermissionList" resultMap="com.kenbings.shop.shopspringsecurityjwt.mbg.mapper.UmsPermissionMapper.BaseResultMap"> SELECT p.* FROM ums_admin_role_relation ar LEFT JOIN ums_role r ON ar.role_id = r.id LEFT JOIN ums_role_permission_relation rp ON r.id = rp.role_id LEFT JOIN ums_permission p ON rp.permission_id = p.id WHERE ar.admin_id = #{adminId} AND p.id IS NOT NULL AND p.id NOT IN ( SELECT p.id FROM ums_admin_permission_relation pr LEFT JOIN ums_permission p ON pr.permission_id = p.id WHERE pr.type = - 1 AND pr.admin_id = #{adminId} ) UNION SELECT p.* FROM ums_admin_permission_relation pr LEFT JOIN ums_permission p ON pr.permission_id = p.id WHERE pr.type = 1 AND pr.admin_id = #{adminId} </select> </mapper>
第十四步 ,在config包内定义一个名为SecurityConfig的类,这是SpringSecurity的配置类,它需要继承WebSecurityConfigurerAdapter
类,并实现其中的一些方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 /** * SpringSecurity配置类 */ @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled=true) public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private UmsAdminService adminService; @Autowired private RestfulAccessDeniedHandler restfulAccessDeniedHandler; @Autowired private RestAuthenticationEntryPoint restAuthenticationEntryPoint; /** * 用于配置需要拦截的url路径、jwt过滤器及出异常后的处理器 * @param httpSecurity * @throws Exception */ @Override protected void configure(HttpSecurity httpSecurity) throws Exception { httpSecurity.csrf()// 由于使用的是JWT,我们这里不需要csrf .disable() .sessionManagement()// 基于token,所以不需要session .sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeRequests() .antMatchers(HttpMethod.GET, // 允许对于网站静态资源的无授权访问 "/", "/*.html", "/favicon.ico", "/**/*.html", "/**/*.css", "/**/*.js", "/swagger-resources/**", "/v2/api-docs/**" ) .permitAll() .antMatchers("/admin/login", "/admin/register")// 对登录注册要允许匿名访问 .permitAll() .antMatchers(HttpMethod.OPTIONS)//跨域请求会先进行一次options请求 .permitAll() // .antMatchers("/**")//测试时全部运行访问 // .permitAll() .anyRequest()// 除上面外的所有请求全部需要鉴权认证 .authenticated(); // 禁用缓存 httpSecurity.headers().cacheControl(); // 添加JWT filter httpSecurity.addFilterBefore(jwtAuthenticationTokenFilter(), UsernamePasswordAuthenticationFilter.class); //添加自定义未授权和未登录结果返回 httpSecurity.exceptionHandling() .accessDeniedHandler(restfulAccessDeniedHandler) .authenticationEntryPoint(restAuthenticationEntryPoint); } /** * 用于配置UserDetailsService及PasswordEncoder * @param auth * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService()) .passwordEncoder(passwordEncoder()); } /** * SpringSecurity定义的用于对密码进行编码及比对的接口,目前使用的是BCryptPasswordEncoder * @return */ @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } /** * SpringSecurity定义的核心接口,用于根据用户名获取用户信息,需要自行实现 * @return */ @Bean public UserDetailsService userDetailsService() { //获取登录用户信息 return username -> { UmsAdmin admin = adminService.getAdminByUsername(username); if (admin != null) { List<UmsPermission> permissionList = adminService.getPermissionList(admin.getId()); return new AdminUserDetails(admin,permissionList); } throw new UsernameNotFoundException("用户名或密码错误"); }; } /** * 在用户名和密码校验前添加的过滤器,如果有jwt的token,会自行根据token信息进行登录 * @return */ @Bean public JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter(){ return new JwtAuthenticationTokenFilter(); } @Bean @Override public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } }
简单解释一下上述一些方法: (1)configure(HttpSecurity httpSecurity)
方法,用于配置需要拦截的url路径、jwt过滤器及出现异常后的处理器; (2)configure(AuthenticationManagerBuilder auth)
方法,用于配置UserDetailsService及PasswordEncoder; (3)passwordEncoder()
方法,这是SpringSecurity定义的用于对密码进行编码及比对的接口,目前使用的是BCryptPasswordEncoder; (4)userDetailsService()
方法,这是SpringSecurity定义用于封装用户信息的类(主要是用户信息和权限),需要开发者自行实现; (5)jwtAuthenticationTokenFilter()
方法,这是在校验用户名和密码前添加的过滤器,如果有jwt且token有效,那么会自行根据token信息进行登录; (6)RestfulAccessDeniedHandler
,这是当访问接口没有权限时,返回自定义的JSON格式结果; (7)RestAuthenticationEntryPoint
,这是当未登录或者token失效访问接口时,返回自定义的JSON格式结果。
第十五步 ,在dto包内定义一个名为UmsAdminLoginParam
的类,这是后台管理员登录参数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 /** * 后台管理员登录参数 */ public class UmsAdminLoginParam { @ApiModelProperty(value = "用户名", required = true) @NotEmpty(message = "用户名不能为空") private String username; @ApiModelProperty(value = "密码", required = true) @NotEmpty(message = "密码不能为空") private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } }
第十六步 ,在controller包内定义一个名为UmsAdminController
的类,这是后台管理员登录注册功能的Controller:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 /** * 后台管理员登录注册功能Controller */ @RestController @Api(tags = "UmsAdminController", description = "后台用户管理") @RequestMapping("/admin") public class UmsAdminController { @Value("${jwt.tokenHeader}") private String tokenHeader; @Value("${jwt.tokenHead}") private String tokenHead; @Autowired private UmsAdminService adminService; @ApiOperation(value = "用户注册") @PostMapping("/register") public CommonResult<UmsAdmin> register(@RequestBody UmsAdmin umsAdminParam, BindingResult result) { if (result.hasErrors()) { return CommonResult.failed(result.getFieldError().getDefaultMessage()); } UmsAdmin umsAdmin = adminService.register(umsAdminParam); if (umsAdmin == null) { return CommonResult.failed(); } return CommonResult.success(umsAdmin); } @ApiOperation(value = "登录后返回token") @PostMapping("/login") public CommonResult login(@Validated @RequestBody UmsAdminLoginParam umsAdminLoginParam, BindingResult result) { if (result.hasErrors()) { return CommonResult.failed(result.getFieldError().getDefaultMessage()); } String token = adminService.login(umsAdminLoginParam.getUsername(), umsAdminLoginParam.getPassword()); if (token == null) { return CommonResult.validateFailed("用户名或密码错误"); } Map<String, String> tokenMap = new HashMap<>(); tokenMap.put("token", token); tokenMap.put("tokenHead", tokenHead); return CommonResult.success(tokenMap); } @ApiOperation("获取用户所有权限(包括+-权限)") @GetMapping("/permission/{adminId}") @ResponseBody public CommonResult<List<UmsPermission>> getPermissionList(@PathVariable @ApiParam("后台用户id")Long adminId) { List<UmsPermission> permissionList = adminService.getPermissionList(adminId); return CommonResult.success(permissionList); } }
可以看到这里我们只提供了三个接口,分别是后台用户登录、注册以及获取对应权限的接口。
第十七步 ,修改config包中Swagger2Config类的代码为如下所示,这里其实就是实现接口调用时自带Authorization头,后续就可以访问需要登录的接口:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 /** * Swagger2 API文档相关配置 */ @Configuration @EnableSwagger2 public class Swagger2Config { @Bean public Docket docket(){ return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() //为当前包下controller生成API文档 .apis(RequestHandlerSelectors.basePackage("com.kenbings.shop.shopspringsecurityjwt.controller")) .paths(PathSelectors.any()) .build() //添加登录认证 .securitySchemes(securitySchemes()) .securityContexts(securityContexts()); } private ApiInfo apiInfo() { return new ApiInfoBuilder() .title("整合SwaggerUI") .description("myshop-all") .contact("kenbings") .version("1.0") .build(); } private List<ApiKey> securitySchemes() { //设置请求头信息 List<ApiKey> result = new ArrayList<>(); ApiKey apiKey = new ApiKey("Authorization", "Authorization", "header"); result.add(apiKey); return result; } private List<SecurityContext> securityContexts() { //设置需要登录认证的路径 List<SecurityContext> result = new ArrayList<>(); result.add(getContextByPath("/brand/.*")); return result; } private SecurityContext getContextByPath(String pathRegex){ return SecurityContext.builder() .securityReferences(defaultAuth()) .forPaths(PathSelectors.regex(pathRegex)) .build(); } private List<SecurityReference> defaultAuth() { List<SecurityReference> result = new ArrayList<>(); AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything"); AuthorizationScope[] authorizationScopes = new AuthorizationScope[1]; authorizationScopes[0] = authorizationScope; result.add(new SecurityReference("Authorization", authorizationScopes)); return result; } }
可以看到我们在securityContexts()
方法中设置了需要登录认证的路径,即品牌相关的接口访问都需要登录之后才能访问。
第十八步 ,修改之前PmsBrandController类中的方法,我们给它们添加如下访问权限: (1)给添加接口添加pms:brand:create
权限:
1 2 @PreAuthorize("hasAuthority('pms:brand:create')") public CommonResult createBrand(@RequestBody PmsBrand pmsBrand)
(2)给删除接口添加pms:brand:delete
权限:
1 2 @PreAuthorize("hasAuthority('pms:brand:delete')") public CommonResult deleteBrand(@PathVariable("id") Long id)
(3)给修改接口添加pms:brand:update
权限:
1 2 3 4 5 6 @PreAuthorize("hasAuthority('pms:brand:update')") public CommonResult updateBrand( @PathVariable("id") Long id, @RequestBody PmsBrand pmsBrandDTO, BindingResult result )
(4)给查询接口添加pms:brand:read
权限:
1 2 3 4 5 6 7 8 9 10 11 12 13 @PreAuthorize("hasAuthority('pms:brand:read')") public CommonResult<List<PmsBrand>> getBrandList() @PreAuthorize("hasAuthority('pms:brand:read')") public CommonResult<CommonPage<PmsBrand>> listBrand( @RequestParam(value = "pageNum",defaultValue = "1") @ApiParam("页码")Integer pageNum, @RequestParam(value = "pageSize",defaultValue = "5") @ApiParam("每页数量") Integer pageSize ) @PreAuthorize("hasAuthority('pms:brand:read')") public CommonResult<PmsBrand> getBrand(@PathVariable("id")Long id)
注意这里面的权限就是ums_permission
数据表中value字段的值,因此值必须按照要求进行书写:
第十九步 ,修改项目启动类ShopSpringSecurityJwtApplication
,在其上面使用@MapperScan
注解来指定扫描的mapper接口路径,注意我们的mapper接口路径有两个,因此需要使用字符串数组来进行设置:
1 2 3 4 5 6 7 @SpringBootApplication @MapperScan({"com.kenbings.shop.shopspringsecurityjwt.mbg.mapper","com.kenbings.shop.shopspringsecurityjwt.dao"}) public class ShopSpringSecurityJwtApplication { public static void main(String[] args) { SpringApplication.run(ShopSpringSecurityJwtApplication.class, args); } }
第二十步 ,启动项目,访问Swagger-UI接口文档地址,即浏览器访问http://localhost:8080/swagger-ui.html
链接,可以看到新的接口已经出现了:
首先是未登录,访问/admin/permission/{adminId}
接口,可以看到返回之前定义的JSON信息:
然后我们尝试进行登录,登录账号为kenbings,密码为kenbings,访问/admin/login
接口:
可以看到我们已经登录成功了,接下来点击右侧的Authorize按钮,在弹框中输入登录接口中获取到的token信息:
之后我们再来访问之前的/admin/permission/{adminId}
接口,可以看到该接口已经可以正常访问了:
然后我们再来访问一些需要权限的接口,如获取商品品牌管理中获取所有品牌信息的接口/brand/listAll
:
这是正常结果,因为当前用户没有任何权限,我们给这个用户可以访问该接口的权限,接着再去访问一下该接口,可以看到数据能正常返回了:
这样本篇关于整合SpringSecurity和JWT实现认证与授权的学习就完成了,后续介绍如何整合ElasticSearch实现商品搜索这一功能。本篇笔记源码,可以点击 这里 进行阅读。