Good morning, everyone!
之前我们已经说过用Shiro和JWT来实现身份认证和用户授权,今天我们再来说一下「Security和JWT」的组合拳。
先赘述一下身份认证和用户授权:
用户认证(Authentication):系统通过校验用户提供的用户名和密码来验证该用户是否为系统中的合法主体,即是否可以访问该系统;
用户授权(Authorization):系统为用户分配不同的角色,以获取对应的权限,即验证该用户是否有权限执行该操作;
Web应用的安全性包括用户认证和用户授权两个部分,而Spring Security(以下简称Security)基于Spring框架,正好可以完整解决该问题。
它的真正强大之处在于它可以轻松扩展以满足自定义要求。
Security可以看做是由一组filter过滤器链组成的权限认证。它的整个工作流程如下所示:
图中绿色认证方式是可以配置的,橘黄色和蓝色的位置不可更改:
我们使用Spring Boot框架来集成。
1.pom文件引入的依赖
org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-tomcat org.springframework.boot spring-boot-starter-undertow mysql mysql-connector-java com.baomidou mybatis-plus-boot-starter 3.4.0 org.projectlombok lombok com.alibaba fastjson 1.2.74 joda-time joda-time 2.10.6 org.springframework.boot spring-boot-starter-test
2.application.yml配置
- spring:
- application:
- name: securityjwt
- datasource:
- driver-class-name: com.mysql.cj.jdbc.Driver
- url: jdbc:mysql://127.0.0.1:3306/cheetah?characterEncoding=utf-8&useSSL=false&serverTimezone=UTC
- username: root
- password: 123456
- server:
- port: 8080
- mybatis:
- mapper-locations: classpath:mapper/*.xml
- type-aliases-package: com.itcheetah.securityjwt.entity
- configuration:
- map-underscore-to-camel-case: true
- rsa:
- key:
- pubKeyFile: C:\Users\Desktop\jwt\id_key_rsa.pub
- priKeyFile: C:\Users\Desktop\jwt\id_key_rsa
3.SQL文件
- /**
- * sys_user_info
- **/
- SET NAMES utf8mb4;
- SET FOREIGN_KEY_CHECKS = 0;
- -- ----------------------------
- -- Table structure for sys_user_info
- -- ----------------------------
- DROP TABLE IF EXISTS `sys_user_info`;
- CREATE TABLE `sys_user_info` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT,
- `username` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
- `password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
- SET FOREIGN_KEY_CHECKS = 1;
- /**
- * product_info
- **/
- SET NAMES utf8mb4;
- SET FOREIGN_KEY_CHECKS = 0;
- -- ----------------------------
- -- Table structure for product_info
- -- ----------------------------
- DROP TABLE IF EXISTS `product_info`;
- CREATE TABLE `product_info` (
- `id` bigint(20) NOT NULL AUTO_INCREMENT,
- `name` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL,
- `price` decimal(10, 4) NULL DEFAULT NULL,
- `create_date` datetime(0) NULL DEFAULT NULL,
- `update_date` datetime(0) NULL DEFAULT NULL,
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE = InnoDB AUTO_INCREMENT = 4 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
- SET FOREIGN_KEY_CHECKS = 1;
org.springframework.boot spring-boot-starter-security io.jsonwebtoken jjwt 0.9.1
引入之后启动项目,会有如图所示:
其中用户名为user,密码为上图中的字符串。
- //开启全局方法安全性
- @EnableGlobalMethodSecurity(prePostEnabled=true, securedEnabled=true)
- public class SecurityConfig extends WebSecurityConfigurerAdapter {
- //认证失败处理类
- @Autowired
- private AuthenticationEntryPointImpl unauthorizedHandler;
- //提供公钥私钥的配置类
- @Autowired
- private RsaKeyProperties prop;
- @Autowired
- private UserInfoService userInfoService;
- @Override
- protected void configure(HttpSecurity httpSecurity) throws Exception {
- httpSecurity
- // CSRF禁用,因为不使用session
- .csrf().disable()
- // 认证失败处理类
- .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()
- // 基于token,所以不需要session
- .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
- // 过滤请求
- .authorizeRequests()
- .antMatchers(
- HttpMethod.GET,
- "/*.html",
- "/**/*.html",
- "/**/*.css",
- "/**/*.js"
- ).permitAll()
- // 除上面外的所有请求全部需要鉴权认证
- .anyRequest().authenticated()
- .and()
- .headers().frameOptions().disable();
- // 添加JWT filter
- httpSecurity.addFilter(new TokenLoginFilter(super.authenticationManager(), prop))
- .addFilter(new TokenVerifyFilter(super.authenticationManager(), prop));
- }
- //指定认证对象的来源
- public void configure(AuthenticationManagerBuilder auth) throws Exception {
- auth.userDetailsService(userInfoService)
- //从前端传递过来的密码就会被加密,所以从数据库
- //查询到的密码必须是经过加密的,而这个过程都是
- //在用户注册的时候进行加密的。
- .passwordEncoder(passwordEncoder());
- }
- //密码加密
- @Bean
- public BCryptPasswordEncoder passwordEncoder(){
- return new BCryptPasswordEncoder();
- }
- }
「拦截规则」
认证失败处理类
- /**
- * 返回未授权
- */
- @Component
- public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable {
- private static final long serialVersionUID = -8970718410437077606L;
- @Override
- public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e)
- throws IOException {
- int code = HttpStatus.UNAUTHORIZED;
- String msg = "认证失败,无法访问系统资源,请先登陆";
- ServletUtils.renderString(response, JSON.toJSONString(AjaxResult.error(code, msg)));
- }
- }
自定义认证过滤器
- public class TokenLoginFilter extends UsernamePasswordAuthenticationFilter {
- private AuthenticationManager authenticationManager;
- private RsaKeyProperties prop;
- public TokenLoginFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
- this.authenticationManager = authenticationManager;
- this.prop = prop;
- }
- /**
- * @author cheetah
- * @description 登陆验证
- * @date 2021/6/28 16:17
- * @Param [request, response]
- * @return org.springframework.security.core.Authentication
- **/
- public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
- try {
- UserPojo sysUser = new ObjectMapper().readValue(request.getInputStream(), UserPojo.class);
- UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(sysUser.getUsername(), sysUser.getPassword());
- return authenticationManager.authenticate(authRequest);
- }catch (Exception e){
- try {
- response.setContentType("application/json;charset=utf-8");
- response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
- PrintWriter out = response.getWriter();
- Map resultMap = new HashMap();
- resultMap.put("code", HttpServletResponse.SC_UNAUTHORIZED);
- resultMap.put("msg", "用户名或密码错误!");
- out.write(new ObjectMapper().writeValueAsString(resultMap));
- out.flush();
- out.close();
- }catch (Exception outEx){
- outEx.printStackTrace();
- }
- throw new RuntimeException(e);
- }
- }
- /**
- * @author cheetah
- * @description 登陆成功回调
- * @date 2021/6/28 16:17
- * @Param [request, response, chain, authResult]
- * @return void
- **/
- public void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
- UserPojo user = new UserPojo();
- user.setUsername(authResult.getName());
- user.setRoles((List
)authResult.getAuthorities()); - //通过私钥进行加密:token有效期一天
- String token = JwtUtils.generateTokenExpireInMinutes(user, prop.getPrivateKey(), 24 * 60);
- response.addHeader("Authorization", "Bearer "+token);
- try {
- response.setContentType("application/json;charset=utf-8");
- response.setStatus(HttpServletResponse.SC_OK);
- PrintWriter out = response.getWriter();
- Map resultMap = new HashMap();
- resultMap.put("code", HttpServletResponse.SC_OK);
- resultMap.put("msg", "认证通过!");
- resultMap.put("token", token);
- out.write(new ObjectMapper().writeValueAsString(resultMap));
- out.flush();
- out.close();
- }catch (Exception outEx){
- outEx.printStackTrace();
- }
- }
- }
流程
Security默认登录路径为/login,当我们调用该接口时,它会调用上边的attemptAuthentication方法;图片图片图片图片所以我们要自定义UserInfoService继承UserDetailsService实现loadUserByUsername方法;
所以我们要自定义UserInfoService继承UserDetailsService实现loadUserByUsername方法;
- public interface UserInfoService extends UserDetailsService {
- }
- @Service
- @Transactional
- public class UserInfoServiceImpl implements UserInfoService {
- @Autowired
- private SysUserInfoMapper userInfoMapper;
- @Override
- public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
- UserPojo user = userInfoMapper.queryByUserName(username);
- return user;
- }
- }
其中的loadUserByUsername返回的是UserDetails类型,所以UserPojo继承UserDetails类
- @Data
- public class UserPojo implements UserDetails {
- private Integer id;
- private String username;
- private String password;
- private Integer status;
- private List
roles; - @JsonIgnore
- @Override
- public Collection extends GrantedAuthority> getAuthorities() {
- //理想型返回 admin 权限,可自已处理这块
- List
auth = new ArrayList<>(); - auth.add(new SimpleGrantedAuthority("ADMIN"));
- return auth;
- }
- @Override
- public String getPassword() {
- return this.password;
- }
- @Override
- public String getUsername() {
- return this.username;
- }
- /**
- * 账户是否过期
- **/
- @JsonIgnore
- @Override
- public boolean isAccountNonExpired() {
- return true;
- }
- /**
- * 是否禁用
- */
- @JsonIgnore
- @Override
- public boolean isAccountNonLocked() {
- return true;
- }
- /**
- * 密码是否过期
- */
- @JsonIgnore
- @Override
- public boolean isCredentialsNonExpired() {
- return true;
- }
- /**
- * 是否启用
- */
- @JsonIgnore
- @Override
- public boolean isEnabled() {
- return true;
- }
- }
当认证通过之后会在SecurityContext中设置Authentication对象,回调调用successfulAuthentication方法返回token信息,
整体流程图如下
自定义token过滤器
- public class TokenVerifyFilter extends BasicAuthenticationFilter {
- private RsaKeyProperties prop;
- public TokenVerifyFilter(AuthenticationManager authenticationManager, RsaKeyProperties prop) {
- super(authenticationManager);
- this.prop = prop;
- }
- public void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
- String header = request.getHeader("Authorization");
- if (header == null || !header.startsWith("Bearer ")) {
- //如果携带错误的token,则给用户提示请登录!
- chain.doFilter(request, response);
- } else {
- //如果携带了正确格式的token要先得到token
- String token = header.replace("Bearer ", "");
- //通过公钥进行解密:验证tken是否正确
- Payload
payload = JwtUtils.getInfoFromToken(token, prop.getPublicKey(), UserPojo.class); - UserPojo user = payload.getUserInfo();
- if(user!=null){
- UsernamePasswordAuthenticationToken authResult = new UsernamePasswordAuthenticationToken(user.getUsername(), null, user.getAuthorities());
- //将认证信息存到安全上下文中
- SecurityContextHolder.getContext().setAuthentication(authResult);
- chain.doFilter(request, response);
- }
- }
- }
- }
当我们访问时需要在header中携带token信息
本文转载自微信公众号「阿Q说代码」,可以通过以下二维码关注。转载本文请联系阿Q说代码公众号。
分享文章:实战篇:Security+JWT组合拳
转载注明:http://www.shufengxianlan.com/qtweb/news9/428759.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源: 创新互联