前言
单体应用中,登录认证的逻辑都写在同一个服务中,我们通常只创建一个用于登录认证的Controller。然而在微服务的系统中,不能像单体应用那样,每个微服务都有用于登录认证的Controller,必须将用户认证的服务抽离成单独的服务,各个服务调用认证服务去认证或者授权。
身份认证和访问控制
身份认证就是验证你是谁,通常用户会在平台上注册一个用户名,使用用户名密码登录,或者使用手机号、第三方登录。身份认证就是将用户前端输入的账号信息,以及后续输入的凭证(密码或者手机验证码)来判断此用户是谁,是否合法。
访问控制即此登录的用户能访问什么资源,通常后台会给予用户一个角色,然后赋予角色可访问的资源。对于身份认证以及访问控制可以使用Spring Security框架。商城项目只是实现了简单的用户登录以及整合Spring Session实现Session共享。
用户创建自己的用户名密码,并使用非对称加密方式传输私密信息。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public void register(RegisterMemberTo to) throws RRException { String mobile = to.getMobile(); String username = to.getUsername(); this.usernameExist(username); this.mobileExist(mobile);
MemberEntity memberEntity = new MemberEntity(); memberEntity.setNickname(username); memberEntity.setUsername(username); memberEntity.setMobile(mobile); BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); memberEntity.setPassword(encoder.encode(to.getPassword())); memberEntity.setCreateTime(new Date()); memberEntity.setStatus(CommonConstant.COMMON_VALID); this.save(memberEntity); }
|
用户使用注册的账号登录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| public UserLoginResp login(UserLoginTo to) { String loginacct = to.getLoginacct();
MemberEntity memberEntity = baseMapper.selectOne(new LambdaQueryWrapper<MemberEntity>() .eq(MemberEntity::getUsername, loginacct) .or().eq(MemberEntity::getMobile, loginacct)); if (memberEntity == null) { throw new RRException(ExceptionMessageEnums.USER_INFO_VALID_FAILED.getMsg(), ExceptionMessageEnums.USER_INFO_VALID_FAILED.getCode()); } String password = memberEntity.getPassword(); BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(); if (!encoder.matches(to.getPassword(), password)) { throw new RRException(ExceptionMessageEnums.USER_INFO_VALID_FAILED.getMsg(), ExceptionMessageEnums.USER_INFO_VALID_FAILED.getCode()); } UserLoginResp resp = new UserLoginResp(); BeanUtils.copyProperties(memberEntity, resp);
return resp; }
|
登录成功之后,共享Session。需要配置Spring Session。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| public String login(LoginDto dto, RedirectAttributes attributes, HttpSession httpSession) { R login = memberFeignService.login(dto); if (R.isOk(login)) { UserLoginResp resp = JSON.parseObject(JSON.toJSONString(login.get("user")), new TypeReference<UserLoginResp>() { }); httpSession.setAttribute(AuthServerConstant.LOGIN_USER, resp); return "redirect:http://easymall.com"; } else { String msg = (String) login.get("msg"); Map<String, String> errors = new HashMap<>(); errors.put("msg", msg); attributes.addFlashAttribute("errors", errors); return "redirect:http://auth.easymall.com/login.html"; } }
|
以上只是简单的实现了用户登录以及Session共享
使用Spring Security,以下实例只实现了用户认证的逻辑。
继承 UsernamePasswordAuthenticationFilter 来实现登录逻辑
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
| public class MyUsernamePasswordAuthenticationFilter extends UsernamePasswordAuthenticationFilter {
private final Logger log = LoggerFactory.getLogger(MyUsernamePasswordAuthenticationFilter.class);
public MyUsernamePasswordAuthenticationFilter(AuthenticationManager authenticationManager) { super.setAuthenticationManager(authenticationManager); }
@SneakyThrows @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { String username = this.obtainUsername(request); String password = this.obtainPassword(request); UserInfo user = new UserInfo(); user.setUsername(username); user.setPassword(password); AuthenticationManager authenticationManager = super.getAuthenticationManager(); return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>())); }
@Override protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { log.info("验证成功"); response.setCharacterEncoding("utf-8"); response.setStatus(200); response.setContentType("application/json"); JSONObject data = new JSONObject(); UserInfo userInfo = (UserInfo) authResult.getPrincipal(); data.put("token", JwtUtil.createToken(userInfo.getUsername() + "", 12L)); data.put("msg", "登录成功"); response.getWriter().println(data.toJSONString()); }
@Override protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException { log.info("验证失败"); response.setCharacterEncoding("utf-8"); response.setStatus(200); response.setContentType("application/json"); JSONObject data = new JSONObject(); data.put("msg", "登录失败"); response.getWriter().println(data.toJSONString()); }
}
|
实现 PasswordEncoder接口 ,实现自己的密码效验逻辑
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
| @Component @Slf4j public class MyPasswordEncoder implements PasswordEncoder {
public static String privateKey = "MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAwpkaFFP7RW0SsByk\n" + "TMx5jWSGP2HwkM9euqeS3KgtB9SlRyBkGHdh6gz/HVH4hB+VgY92yDAdbuYo9iMq\n" + "UFByPwIDAQABAkBxeur0c80SPXsqbGl7x7oStE59Y9Xv/J4XZ2WDfCdWaqyAjoys\n" + "lm6kxRHRE3Iuwtj1qSy6d9+32WylKoq76RhxAiEA7FZouMRdjVRdREcwNJFdY5jR\n" + "IBXM8jDAEVPztlbPLj0CIQDSybZVsDk1/5cdrWI499sZ/g6VQy0f9ukDSw7OPUtG\n" + "KwIgbjrTNzJnS+7IXXsykaInQ7fX+jYQ0/lG7A4TAr20fiUCIQCHD0gNT4TY3JPv\n" + "KBEvf2CcHvUpHonjWUmkGou6CfwZUQIgMquZw4hoWeFiuR1XhDjZprBoDvmNcnrJ\n" + "WoiI15v7ys0=";
private static String publicKey = "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAMKZGhRT+0VtErAcpEzMeY1khj9h8JDP\n" + "XrqnktyoLQfUpUcgZBh3YeoM/x1R+IQflYGPdsgwHW7mKPYjKlBQcj8CAwEAAQ==";
@Override public String encode(CharSequence charSequence) { if (charSequence != null) { return md5(charSequence.toString()); } return ""; }
@Override public boolean matches(CharSequence charSequence, String s) { String password = null; try { password = decryptByPrivateKey(privateKey, charSequence.toString()); } catch (Exception e) { log.error("密码匹配失败 " + e.getMessage() + Arrays.toString(e.getStackTrace())); return false; } return this.encode(password).equals(s);
}
public static String decryptByPrivateKey(String privateKeyText, String text) throws Exception { PKCS8EncodedKeySpec pkcs8EncodedKeySpec5 = new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKeyText)); KeyFactory keyFactory = KeyFactory.getInstance("RSA"); PrivateKey privateKey = keyFactory.generatePrivate(pkcs8EncodedKeySpec5); Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.DECRYPT_MODE, privateKey); byte[] result = cipher.doFinal(Base64.decodeBase64(text)); return new String(result); }
public static String md5(String plainText) { byte[] secretBytes = null; try { secretBytes = MessageDigest.getInstance("md5").digest( plainText.getBytes()); } catch (NoSuchAlgorithmException e) { throw new RuntimeException("没有md5这个算法!"); } String md5code = new BigInteger(1, secretBytes).toString(16); for (int i = 0; i < 32 - md5code.length(); i++) { md5code = "0" + md5code; } return md5code; }
}
|
实现UserDetailsService接口,自定义用户信息效验
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| @Component public class MyUserDetailService implements UserDetailsService {
public Map<String, String> passwordMap = new HashMap<String, String>() {{ put("chirs", "e10adc3949ba59abbe56e057f20f883e"); }};
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { String password; if (passwordMap.containsKey(username)) { password = passwordMap.get(username); } else { throw new UsernameNotFoundException("用户名不存在"); } return new User(username, password, new ArrayList<>()); } }
|
继承 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
| @EnableWebSecurity @Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired private MyUserDetailService userDetailsService; @Autowired private MyPasswordEncoder myPasswordEncoder;
@Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests() .anyRequest().authenticated(); http.csrf().disable(); http.logout(); http.formLogin(); http.addFilter(new MyUsernamePasswordAuthenticationFilter(authenticationManager())); }
@Bean @Override protected AuthenticationManager authenticationManager() throws Exception { return super.authenticationManager(); }
}
|
总结
认证服务做的比较简单,简单的实现了用户认证以及父子域名Session共享。对于用户登录认证可以采用手机验证码登录,以及第三方登录,如微信、微博等。访问控制需要根据具体的业务需求,实现相应的功能。