从零开始搭建一个电商项目-认证服务(七)

  1. 前言

    单体应用中,登录认证的逻辑都写在同一个服务中,我们通常只创建一个用于登录认证的Controller。然而在微服务的系统中,不能像单体应用那样,每个微服务都有用于登录认证的Controller,必须将用户认证的服务抽离成单独的服务,各个服务调用认证服务去认证或者授权。

  2. 身份认证和访问控制

    身份认证就是验证你是谁,通常用户会在平台上注册一个用户名,使用用户名密码登录,或者使用手机号、第三方登录。身份认证就是将用户前端输入的账号信息,以及后续输入的凭证(密码或者手机验证码)来判断此用户是谁,是否合法。
    访问控制即此登录的用户能访问什么资源,通常后台会给予用户一个角色,然后赋予角色可访问的资源。对于身份认证以及访问控制可以使用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<>()));
}


/**
* 登录成功逻辑
*
* @param request
* @param response
* @param chain
* @param authResult
* @throws IOException
* @throws ServletException
*/
@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);

}


/**
* 私钥解密
*
* @param privateKeyText
* @param text
* @return
* @throws Exception
*/
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);
}

/**
* 使用md5的算法进行加密
*/
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);// 16进制数字
// 如果生成数字未满32位,需要前面补0
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();
}


}
  1. 总结

    认证服务做的比较简单,简单的实现了用户认证以及父子域名Session共享。对于用户登录认证可以采用手机验证码登录,以及第三方登录,如微信、微博等。访问控制需要根据具体的业务需求,实现相应的功能。

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!

请我喝杯咖啡吧~

支付宝
微信