Shiro框架实现登录验证和授权，以及客户端持久化登录
二、大致内容实现用户登录和登录状态持久化，以及权限验证三、效果首现，通过运用shiro框架并设置好Fliter过滤器，并且设置好过滤规则，可以将所有请求都拦截，并且在配置之后，shiro框架会接管Tomcat的会话管理。因为HTTP请求是没有状态的，因此Tomcat自身会产生一个sessionID来对每个会话进行标记，而shiro框架对这一过程进行了接管，并能按自己的配置设置这个sessionID在cookie中的字段名。然后使用缓存，例如ehcache，redis等，进行这个sessionID的存储。我这里将其命名为sid。shiro框架中，请求经过过滤器后，会交给SecurityManager进行处理，而SecurityManager一般又从Realm中获取验证数据，所以我要结合我的数据接口的情况来自定义一个Realm，而它一般要实现两个功能，身份验证和权限授予，分别在doGetAuthorizationInfo()和doGetAuthenticationInfo()中进行，返回的是AuthorizationInfo对象和AuthenticationInfo对象。这两个分别表示着授权信息和验证信息。在我的代码中，对于前者，我通过setRoles()函数向其中添加了和当前用户有关的角色信息，并且通过setStringPermissions()函数向其中添加了和当前用户有关的权限信息，然后返回。而对于后者，在构建authenticationInfo的时候，使用了四个参数，分别是登录者发送的用户名和密码，还有随机生成的盐值，以及这个realm的类名。再到用户验证，那么要匹配登录者发送的信息和数据库中对应的信息，就会有个验证和匹配的地方，这个匹配的借口我在RetryLimitHashedCredentialsMatcher类中实现，它继承了HashedCredentialsMatcher这个类，这里的匹配和验证本质上是交给父类去做的，而我增加的内容，则是从缓存中取出现在登录的这个用户名，它的连续错误登录次数，若是过多，则会抛出ExcessiveAttemptsException。这些以上的内容，都会在配置文件spring-shiro.xml中用到大概的顺序级和关系大概如下：未命名文件-2然后再到我的登录入口，我经常会用到Subject对象，这个对象存有用户相关的各种信息，例如用户名，sessionID等，登录和登出操作也是以它为入口进行的。而Subject对象为什么能存储呢，通过观看源码可以发现，它从SecurityUtils中返回，而这又是从ThreadContext.getSubject()中获取的，再接着看，创建Subject的时候，是将其和ThreadContext绑定的，相当于与线程绑定了。然后演示一下首先进行登录，试试输错的情况：屏幕快照 2019-06-23 23.47.36.png然后成功登录，并在返回信息中加入用户相关的信息，如昵称，性别，头像url等：屏幕快照 2019-06-23 23.54.47.png然后可以看看cookie中的sid：屏幕快照 2019-06-23 23.48.44.png然后进行信息的请求，这个API是需要用户登录的，因此登录了的用户可以成功通过：屏幕快照 2019-06-23 23.52.57.png然后删除sid，此刻已经相当于未登录状态，即一个新的会话，然后再次请求刚刚的那个API：屏幕快照 2019-06-23 23.53.18.png出现错误了，即表明shiro框架的验证和匹配是成功的。再试试看持久登录，rememberMe的选项：屏幕快照 2019-06-24 00.17.33.png可以看到，cookie中多了rememberMe的key-value，这个就是用来存储在客户端，能够保持持久登录的字段和密文。试试重启服务器，并且删去sid：屏幕快照 2019-06-24 00.20.17.png依然是成功获取了，这表明持久化登录的功能是成功了的四、代码详情和解析我选择的是shiro框架来实现用户登录和验证的功能，正好附带了权限验证的功能，能够在后续实现用户的锁定，设置超级管理员等等，可以方便App的功能升级。而权限验证也方便了Api的开发。首先需要改变表的结构，当然，可以结合之前的表，将之前的表改为userinfo，对应的mapper，service，dao和controller也改为userinfoxxx，然后在数据库中添加如下表格：create table sys_users ( id bigint auto_increment comment '编号', username varchar(100) comment '用户名', password varchar(100) comment '密码', salt varchar(100) comment '盐值', role_id varchar(50) comment '角色列表', locked bool default false comment '是否锁定', constraint pk_sys_users primary key(id) ) charset=utf8 ENGINE=InnoDB; create unique index idx_sys_users_username on sys_users(username); create table sys_roles ( id bigint auto_increment comment '角色编号', role varchar(100) comment '角色名称', description varchar(100) comment '角色描述', pid bigint comment '父节点', available bool default false comment '是否锁定', constraint pk_sys_roles primary key(id) ) charset=utf8 ENGINE=InnoDB; create unique index idx_sys_roles_role on sys_roles(role); create table sys_permissions ( id bigint auto_increment comment '编号', permission varchar(100) comment '权限编号', description varchar(100) comment '权限描述', rid bigint comment '此权限关联角色的id', available bool default false comment '是否锁定', constraint pk_sys_permissions primary key(id) ) charset=utf8 ENGINE=InnoDB; create unique index idx_sys_permissions_permission on sys_permissions(permission); create table sys_users_roles ( id bigint auto_increment comment '编号', user_id bigint comment '用户编号', role_id bigint comment '角色编号', constraint pk_sys_users_roles primary key(id) ) charset=utf8 ENGINE=InnoDB; create table sys_roles_permissions ( id bigint auto_increment comment '编号', role_id bigint comment '角色编号', permission_id bigint comment '权限编号', constraint pk_sys_roles_permissions primary key(id) ) charset=utf8 ENGINE=InnoDB;这是实现之后的表：屏幕快照 2019-06-23 22.32.32其中test和user是测试用表，和项目整体无关。然后要实现User，Role，Permission的mapper，dao和service，这里还是比较基础和底层的内容。然后是Service的实现类：UserServcieImpl：@Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Autowired private PasswordHelper passwordHelper; /** * 添加用户-角色关系 * * @param userId * @param roleIds */ @Transactional public void correlationRoles(Long userId, Long... roleIds) { if (roleIds == null || roleIds.length == 0) { return; } for (Long roleId: roleIds) { if (!exists(userId, roleId)) { userDao.correlationRoles(userId, roleId); } } } /** * 移除用户-角色关系 * * @param userId * @param roleIds */ @Transactional public void uncorrelationRoles(Long userId, Long... roleIds) { if (roleIds == null || roleIds.length == 0) { return; } for (Long roleId: roleIds) { if (exists(userId, roleId)) { userDao.uncorrelationRoles(userId, roleId); } } } /** * 判断当前的用户和角色是否存在 * * @param userId * @param roleId * @return */ @Transactional public boolean exists(Long userId, Long roleId) { return userDao.exists(userId, roleId); } @Transactional @Override public List<Role> findRoles(String username) { return userDao.findRoles(username); } @Transactional @Override public List<Permission> findPermissions(String username) { return userDao.findPermissions(username); } @Transactional @Override public void changePassword(Long id, String newPassword) { //User user = ((UserServiceImpl)AopContext.currentProxy()).findById(id); User user = userDao.findById(id); // 因为数据库中`locked`字段使用的类型：`tinyint(1)`， // 那么使用mybatis查询数据库会自动将数据转换成boolean类型(使用了boolean类型接收)，0：false；1或其他非零数字：true System.out.println("是否锁定：" + user.getLocked()); if (user==null){ return; } user.setPassword(newPassword); passwordHelper.encryptPassword(user); userDao.update(user); } @Transactional @Override public void create(User user) { //加密密码 passwordHelper.encryptPassword(user); userDao.create(user); } @Transactional @Override public void delete(Long id) { userDao.delete(id); } @Transactional @Override public void update(User user) { //加密密码 passwordHelper.encryptPassword(user); userDao.update(user); } @Transactional @Override public List<User> findAll() { return userDao.findAll(); } @Transactional @Override public User findByName(String username) { User user = userDao.findByName(username); if (user == null) { return null; } return user; } @Transactional @Override public User findById(Long id) { User user = userDao.findById(id); // 因为数据库中`locked`字段使用的类型：`tinyint(1)`， // 那么使用mybatis查询数据库会自动将数据转换成boolean类型(使用了boolean类型接收)，0：false；1或其他非零数字：true System.out.println("是否锁定：" + user.getLocked()); if (user == null) { return null; } return user; } @Transactional @Override public void deleteAllUserRoles(Long id) { userDao.deleteAllUserRoles(id); }RoleServiceImpl：@Service public class RoleServiceImpl implements RoleService { @Autowired private RoleDao roleDao; @Transactional @Override public void correlationPermissions(Long roleId, Long... permissionIds) { if(permissionIds == null || permissionIds.length == 0){ return; } for(Long permissionId : permissionIds){ if(!exists(roleId, permissionId)){ roleDao.correlationPermissions(roleId,permissionId); } } } @Transactional @Override public void uncorrelationPermissions(Long roleId, Long... permissionIds) { if(roleId == null || permissionIds.length == 0){ return; } for(Long permissionId : permissionIds){ if(exists(roleId, permissionId)){ roleDao.uncorrelationPermissions(roleId, permissionId); } } } @Transactional @Override public Role findById(Long id) { return roleDao.findById(id); } /** * 查询表中是否存在此数据 * @param roleId * @param permissionId * @return */ private boolean exists(Long roleId, Long permissionId) { return roleDao.exists(roleId, permissionId); } @Transactional @Override public void create(Role role) { if (role.getPid() == null){ role.setPid(0L); } roleDao.create(role); } @Transactional @Override public void delete(Long id) { //先将和角色相关的表删除 roleDao.deleteUserRole(id); //再删除角色表数据 roleDao.deleteRole(id); } @Transactional @Override public List<Role> findAll() { return roleDao.findAll(); } @Transactional @Override public List<Permission> findRolePermissionByRoleId(Long id) { return roleDao.findRolePermissionByRoleId(id); } @Transactional @Override public List<Permission> findPermissionByRoleId(Long id) { return roleDao.findPermissionByRoleId(id); } @Transactional @Override public void update(Role role) { roleDao.update(role); } @Transactional @Override public void deleteAllRolePermissions(Long id) { roleDao.deleteAllRolePermissions(id); } @Transactional @Override public void updateUserRole_Id(Role role) { roleDao.updateUserRole_Id(role); } }以及PermissionServiceImpl：@Service public class PermissionServiceImpl implements PermissionService { @Autowired private PermissionDao permissionDao; @Transactional @Override public void create(Permission permission) { permissionDao.create(permission); } @Transactional @Override public void delete(Long id) { //先将和Permission相关的表数据删除 permissionDao.deleteRolePermission(id); //再删除Permission表数据 permissionDao.deletePermission(id); } @Transactional @Override public List<Permission> findAll() { return permissionDao.findAll(); } @Transactional @Override public void update(Permission permission) { permissionDao.update(permission); } @Transactional @Override public Permission findById(Long id) { return permissionDao.findById(id); } @Transactional @Override public List<Role> findRoleByPermissionId(Long id) { return permissionDao.findRoleByPermissionId(id); } @Transactional @Override public void deleteAllPermissionsRoles(Long id) { permissionDao.deleteAllPermissionsRoles(id); } @Transactional @Override public void correlationRoles(Long permissionId, Long roleId) { permissionDao.correlationRoles(permissionId, roleId); } }然后在创建用户的时候，要将用户的用户名和密码存入数据库，密码并不明文存储，那么就要通过一个工具类来进行加密：@Component public class PasswordHelper { //实例化RandomNumberGenerator对象，用于生成一个随机数 private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator(); //散列算法名称 private String algorithName = "MD5"; //散列迭代次数 private int hashInterations = 2; public RandomNumberGenerator getRandomNumberGenerator() { return randomNumberGenerator; } public void setRandomNumberGenerator(RandomNumberGenerator randomNumberGenerator) { this.randomNumberGenerator = randomNumberGenerator; } public String getAlgorithName() { return algorithName; } public void setAlgorithName(String algorithName) { this.algorithName = algorithName; } public int getHashInterations() { return hashInterations; } public void setHashInterations(int hashInterations) { this.hashInterations = hashInterations; } //加密算法 public void encryptPassword(User user){ if (user.getPassword() != null){ //对user对象设置盐：salt；这个盐值是randomNumberGenerator生成的随机数，所以盐值并不需要我们指定 //spring-shiro.xml中,storedCredentialsHexEncoded=true 则需要.toHex() //spring-shiro.xml中,storedCredentialsHexEncoded=false 则需要.toBase64() user.setSalt(randomNumberGenerator.nextBytes().toHex()); //调用SimpleHash指定散列算法参数：1、算法名称；2、用户输入的密码；3、盐值（随机生成的）；4、迭代次数 //user实体的方法getCredentialsSalt获取的salt'=username+salt，而salt为一串随机序列 String newPassword = new SimpleHash( algorithName, user.getPassword(), ByteSource.Util.bytes(user.getCredentialsSalt()), hashInterations).toHex(); user.setPassword(newPassword); } } }然后就要实现shiro框架中最重要的内容了，Realm类：UserRealm：public class UserRealm extends AuthorizingRealm { @Autowired private UserService userService; /** * 权限校验 * @param principals * @return */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("权限校验--执行了doGetAuthorizationInfo..."); String username = (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //注意这里的setRoles和setStringPermissions需要的都是一个Set<String>类型参数 Set<String> role = new HashSet<String>(); List<Role> roles = userService.findRoles(username); for (Role r : roles){ role.add(r.getRole()); } authorizationInfo.setRoles(role); Set<String> permission = new HashSet<String>(); List<Permission> permissions = userService.findPermissions(username); for (Permission p : permissions){ permission.add(p.getPermission()); } authorizationInfo.setStringPermissions(permission); return authorizationInfo; } /** * 身份校验 * @param token * @return * @throws AuthenticationException */ protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("身份校验--执行了goGetAuthenticationInfo..."); //token.getCredentials().toString(); //credentials这个属性，在UsernamePasswordToken中其实是个Object，查看源代码，getCredentials()方法返回的就是password //若要正确得到UsernamePasswordToken的password，可以将credentials转为char[]再String.valof()方法获得String String username = (String) token.getPrincipal(); User user = userService.findByName(username); if (user == null) { throw new UnknownAccountException(); //没有找到账号 } if (Boolean.TRUE.equals(user.getLocked())) { throw new LockedAccountException(); //账号锁定 } //交给AuthenticationRealm使用CredentialsMatcher进行密码匹配 //SimpleAccount则可以直接获取明文 //user实体的方法getCredentialsSalt获取的salt'=username+salt，而salt为一串随机序列 //在PasswordHelper中，同样是用这种方式作为盐值传给SimpleHash来加密 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user.getUsername(), //用户名 user.getPassword(), //密码 ByteSource.Util.bytes(user.getCredentialsSalt()), getName() //realm name ); return authenticationInfo; } @Override public void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } @Override public void clearCachedAuthenticationInfo(PrincipalCollection principals) { super.clearCachedAuthenticationInfo(principals); } @Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); } public void clearAllCachedAuthorizationInfo() { getAuthorizationCache().clear(); } public void clearAllCachedAuthenticationInfo() { getAuthenticationCache().clear(); } public void clearAllCache() { clearAllCachedAuthenticationInfo(); clearAllCachedAuthorizationInfo(); } }Authentication的目的是对用户的密码进行校验，Authorization的目的是对用户的权限进行校验，前者主要在登录的过程进行，后者则在用户请求设定了权限要求的API的实现进行。这里是负责获取数据库的信息和用户传入的信息，真正校验的类，在另一个地方。RetryLimitHashedCredentialsMatcher：public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher { private Cache<String, AtomicInteger> passwordRetryCache; public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager){ passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String username = (String) token.getPrincipal(); //return count+1 AtomicInteger retryCount = passwordRetryCache.get(username); System.out.println("prove the cache is OK: " + retryCount); if(retryCount == null){ retryCount = new AtomicInteger(0); passwordRetryCache.put(username,retryCount); } if(retryCount.incrementAndGet() > 5){ throw new ExcessiveAttemptsException(); } boolean matches = super.doCredentialsMatch(token,info); if(matches){ //clear retry count passwordRetryCache.remove(username); } return matches; } }这里会从缓存中调取用户登录的信息，看看是否存在多次登录的状况。然后调用父类的方法进行验证。然后，就要配置了，这方面还是很麻烦的。首先是ehcache.xml，这是为了能够让session持久化，以及能够提升效率<?xml version="1.0" encoding="UTF-8"?> <ehcache name="shirocache"> <diskStore path="java.io.tmpdir"/> <cache name="passwordRetryCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="authorizationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="authenticationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> <cache name="shiro-activeSessionCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true"> </cache> </ehcache>然后是log4j.properties：#Console log4j.appender.Console=org.apache.log4j.ConsoleAppender log4j.appender.console.Target=System.out log4j.appender.Console.layout=org.apache.log4j.PatternLayout log4j.appender.Console.layout.ConversionPattern=%d [%t] %-5p [%c] - %m%n log4j.logger.org.apache=ERROR log4j.logger.org.mybatis=ERROR log4j.logger.org.springframework=ERROR # Default Shiro logging log4j.logger.org.apache.shiro=ERROR #这个须要 log4j.logger.log4jdbc.debug=ERROR log4j.logger.com.gk.mapper=ERROR log4j.logger.jdbc.audit=ERROR log4j.logger.jdbc.resultset=ERROR #这个打印SQL语句非常重要 log4j.logger.jdbc.sqlonly=DEBUG log4j.logger.jdbc.sqltiming=ERROR log4j.logger.jdbc.connection=FATAL最后是最重要的spring-shiro.xml：<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:util="http://www.springframework.org/schema/util" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd"> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:other/ehcache.xml"/> </bean> <bean id="credentialsMatcher" class="com.mySSM.credentials.RetryLimitHashedCredentialsMatcher"> <constructor-arg ref="cacheManager"/> <property name="hashAlgorithmName" value="md5"/> <property name="hashIterations" value="2"/> <property name="storedCredentialsHexEncoded" value="true"/> </bean> <bean id="userRealm" class="com.mySSM.realm.UserRealm"> <property name="credentialsMatcher" ref="credentialsMatcher"/> <property name="cachingEnabled" value="true"/> <property name="authenticationCachingEnabled" value="true"/> <property name="authenticationCacheName" value="authenticationCache"/> <property name="authorizationCachingEnabled" value="true"/> <property name="authorizationCacheName" value="authorizationCache"/> </bean> <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/> <bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="sid"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="-1"/> </bean> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe"/> <property name="httpOnly" value="true"/> <property name="maxAge" value="2592000"/> </bean> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}"/> <property name="cookie" ref="rememberMeCookie"/> </bean> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/> <property name="sessionIdGenerator" ref="sessionIdGenerator"/> </bean> <bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler"> <property name="sessionValidationInterval" value="1800000"/> <property name="sessionManager" ref="sessionManager"/> </bean> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="globalSessionTimeout" value="1800000"/> <property name="deleteInvalidSessions" value="true"/> <property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/> <property name="sessionDAO" ref="sessionDAO"/> <property name="sessionIdCookieEnabled" value="true"/> <property name="sessionIdCookie" ref="sessionIdCookie"/> </bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="userRealm"/> <property name="sessionManager" ref="sessionManager"/> <property name="cacheManager" ref="cacheManager"/> <property name="rememberMeManager" ref="rememberMeManager"/> </bean> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/> <property name="arguments" ref="securityManager"/> </bean> <bean id="formAuthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter"> <property name="usernameParam" value="username"/> <property name="passwordParam" value="password"/> <property name="loginUrl" value="/login"/> <property name="rememberMeParam" value="rememberMe"/> </bean> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login"/> <!--<property name="successUrl" value="/index.do"/>--> <property name="unauthorizedUrl" value="/unauthorized"/> <property name="filters"> <util:map> <entry key="authc" value-ref="formAuthenticationFilter"/> </util:map> </property> <property name="filterChainDefinitions"> <value> /static/** = anon /lib/** = anon /js/** = anon /resources/** = anon /login.jsp = anon /login.do = anon /login = anon /userinfo/** = anon /testsid = anon /testsid.jsp = anon /testsid.do = anon /initial = anon /logout = logout /index.jsp = user /authenticated.jsp = authc /** = user </value> </property> </bean> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/> </beans>然后在spring-web.xml中添加如下内容： <aop:config proxy-target-class="true"/> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>最后在web.xml中添加过滤器： <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <async-supported>true</async-supported> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> 然后完成对应的controller的编写即可，例如登录：@Controller public class LoginController { /** * 用户登录的入口 * * @param username * @param password * @param model * @return */ @RequestMapping("/login") public String login( @RequestParam(value = "username", required = false) String username, @RequestParam(value = "password", required = false) String password, @RequestParam(value = "remember", required = false) String remember, Model model) { System.out.println("登陆用户输入的用户名：" + username + "，密码：" + password); String error = null; if (username != null && password != null) { //初始化 Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken(username, password); if (remember != null){ if (remember.equals("on")) { //说明选择了记住我 token.setRememberMe(true); } else { token.setRememberMe(false); } }else{ token.setRememberMe(false); } try { //登录，即身份校验，由通过Spring注入的UserRealm会自动校验输入的用户名和密码在数据库中是否有对应的值 subject.login(token); System.out.println("用户是否登录：" + subject.isAuthenticated()); return "redirect:index.do"; } catch (UnknownAccountException e) { e.printStackTrace(); error = "用户账户不存在，错误信息：" + e.getMessage(); } catch (IncorrectCredentialsException e) { e.printStackTrace(); error = "用户名或密码错误，错误信息：" + e.getMessage(); } catch (LockedAccountException e) { e.printStackTrace(); error = "该账号已锁定，错误信息：" + e.getMessage(); } catch (DisabledAccountException e) { e.printStackTrace(); error = "该账号已禁用，错误信息：" + e.getMessage(); } catch (ExcessiveAttemptsException e) { e.printStackTrace(); error = "该账号登录失败次数过多，错误信息：" + e.getMessage(); } catch (Exception e){ e.printStackTrace(); error = "未知错误，错误信息：" + e.getMessage(); } } else { error = "请输入用户名和密码"; } //登录失败，跳转到login页面 model.addAttribute("error", error); return "login"; } // /** // * 退出登录，我们不需要实现，Shiro的Filter过滤器会帮我们生成一个logout请求， // * 当浏览器访问`/logout`请求时，Shiro会自动清空缓存并重定向到配置好的loginUrl页面 // * // * @return // */ // @RequestMapping("/logout") // public String logout() { // Subject subject = SecurityUtils.getSubject(); // subject.logout(); // return "index"; // } /** * 登录成功，跳转到首页 * * @return */ @RequestMapping("/index") public String index() { return "index"; }四、遇到的问题1.首先在配置完，启动的时候，报错了，报错信息是Error creating bean with name 'shiroFilter': Requested bean is currently in creation: Is there an unresolvable circular reference?，然后差了一下，发现各种可能性都有，最后发现，我之前为了实现在service中能够调用自身的方法，<aop:aspectj-autoproxy expose-proxy="true"/> ，而在配置shiro的时候，又用了 <aop:config proxy-target-class="true"/> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>因此混用产生了冲突，去掉之前那个就好，然后改了一下service。参考：Spring AOP 不同配置方式产生的冲突问题2.接着是sessionID的问题，我用的服务器是Tomcat，会自动产生一个sessionID并存入cookie中， 然而shiro配置了之后也会产生一个sessionID，我定义的名字是sid，而tomcat产生的叫JSESSIONID，然后我在controller中打印HttpSession和Subject.GetSession的id，发现是一样的，意味着JSESSIONID应该是没有用的，也即shiro接管了sessionid的产生等一系列过程。那么要如何去除tomcat自己产生的sessionid呢，在jsp文件中加入session="false"即可，运行了一次之后，发现往后所有连接的cookie中都不带有JSESSIONID了。五、感想这次因为量有点大，并且bug出的多。不过收获还是很足的，用户登录功能可以说是基础，后续只要在这上面添砖加瓦就可以了。
一、目录用户表的设计重定向和转发shiro框架初探二、实验结果和学习内容用户表的设计一开始，用于测试的时候，用户表就是简单的username加password两个字段，而为了后续的功能实现，我需要增加一个userID，并且尽量使得不能存在溢出的可能（当作亿级用户量来处理）。并且如果应用支持第三方登录，或者只使用第三方登录，那么第三方的 User ID 有可能是 64 位的。虽然自己的应用不一定能增长到超过 32 位，但是第三方很可能是 64 位的。然后还要增加用户状态 status 判断用户是否被封禁，注册时间和注册IP地址、上次登录时间和IP地址备查（并衍生出登录记录表，用来判断是否异地登录等，在此不表），用户角色/权限 role （又衍生出用户角色权限关系），业务也需要个人的个人信息如真实姓名、地址等也要往上添加，现在形成了一个很完整的用户关系表。在这之后，微博开放了第三方网站登录，用微博帐号就能登录。那就要增加个微博用户登录表，并且和本来的用户表关联，则这个微博用户信息表如下：id 自增ID user_id 关联本站用户ID uid 微博唯一ID access_token access_expire不仅如此，算上QQ和微信，一下子要接入好多家第三方登录了，只能就着“微博用户信息表”继续加类型加判断，如果是每个第三方登录都新建一个表，肯定会很冗杂。而作为iOS应用，必须要支持手机号登录。所以表结构如下：用户表 id username email phone … 用户第三方登录表 id user_id app_type app_user_id access_token进行改进无论username+password，还是phone+password，都是一种用户信息+密码的验证形式；再来理解第三方登录，其实它也是用户信息+密码的形式，用户信息即第三方系统中的ID（第三方登录一定会给一个在他们系统中的唯一标识），密码即access_token，只不过是一种有使用时效定期修改的密码。所以我们把它抽象出了用户基础信息表加上用户授权信息表的形式。用户基础信息表 users id nickname avatar 用户授权信息表 user_auths id user_id identity_type 登录类型（手机号 邮箱 用户名）或第三方应用名称（微信 微博等） identifier 标识（手机号 邮箱 用户名或第三方应用的唯一标识） credential 密码凭证（站内的保存密码，站外的不保存或保存token）这个系统最大的特色就是，用户信息表不保存任何密码，不保存任何登录信息（如用户名、手机号、邮箱），只留有昵称、头像等基础信息。所有和授权相关（且基本前端展示无关的），都放在用户信息授权表，用户信息表和用户授权表是一对多的关系。例如：usersidnicknameavatar1慕容雪村http://…/avatar.jpg2魔力鸟http://…/avatar2.jpg3科比http://…/avatar3.jpguser_authsiduser_ididentity_typeidentifiercredential11email123@example.compassword_hash(密码)21phone13888888888password_hash(密码)31weibo微博UID微博access_token42usernamemoliniaopassword_hash(密码)53weixin微信UserName微信token具体处理如下，当用户发来邮箱/用户名/手机号和密码请求登录的时候，依然是先判断类型，以某用户使用了手机号登录为例，使用 SELECT * FROM user_auths WHERE type=’phone’ and identifier=’手机号’ 查找条目，如有，取出并判断password_hash(密码)是否和该条目的credential相符，相符则通过验证，随后通过user_id获取用户信息。如果使用第三方登录，则只要判断 SELECT * FROM user_auths WHERE type=’weixin’ and identifier=’微信UserName’ ，如果有记录，则直接登录成功，使用新的token更新原token。假设与微信服务器通信不被劫持的情况下无需判断凭证问题。--重定向和转发然后由于功能实现需要用到转发或者重定向的功能，因此了解了一下：1.使用HttpServletResponse 重定向到另一个视图 @RequestMapping("/resp") public void handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception { resp.sendRedirect("index.jsp"); }2.使用HttpServletRequest 转发（默认访问/下的index.jsp页面 不受渲染器的影响） @RequestMapping("/resp") public void handleRequest(HttpServletRequest req, HttpServletResponse resp) throws Exception { req.setAttribute("message","it's forword "); req.getRequestDispatcher("index.jsp").forward(req,resp); }3.直接返回jsp页面的名称（无渲染器） @RequestMapping("/nice") public String hello1(){ //转发方式1 return "home.jsp"; //转发方式2 return "forward:index.jsp"; //重定向方式 return "redirect:index.jsp"; } 4.当有渲染器指定的时候@RequestMapping("/nice") public String hello1(){ //转发方式1 return "home"; //转发方式2 return "forward:index"; //重定向方式 hello指的是requsrmapping return "redirect:hello"; }5.使用ModelAndView需要视图解析器 能指定跳转页面public class HelloController implements Controller { @Override public ModelAndView handleRequest(javax.servlet.http.HttpServletRequest httpServletRequest, javax.servlet.http.HttpServletResponse httpServletResponse) throws Exception { ModelAndView mv = new ModelAndView(); //封装要显示到视图的数据 mv.addObject("msg","hello myfirst mvc"); //视图名 mv.setViewName("hello"); return mv; } }需要进行配置： <bean class="org.springframework.web.servlet.mvc.SimpleControllerHandlerAdapter" /> <bean id="viewResolver" class="org.springframework.web.servlet.view.UrlBasedViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"/> </bean> <bean name="/hello.do" class="com.jsu.mvc.HelloController"></bean> 6.使用ModelView //通过modelmap方式 @RequestMapping("/modelmap") public String modelHello(String name,ModelMap map){ map.addAttribute("name",name); System.out.println(name); return "index.jsp"; }并且重定向和转发的具体效果都是跳转到别的页面，但实际上两者的区别不尽相同。sendRedirect()是在用户的浏览器端工作，同时它可以重定向至不同的主机上，sendRedirect()可以重定向有frame的jsp文件。假设转发代码包含于注册的servlet－url为/ggg/tt；jsp为/ggg/tt.jsp。绝对路径：response.sendRedirect("http://www.brainysoftware.com")发送至http://www.brainysoftware.com根路径：response.sendRedirect("/ooo")发送至http://localhost:8080/ooo相对路径：response.sendRedirect("ooo")发送至http://localhost:8080/Test/ggg/ooo。forward()无法重定向至有frame的jsp文件,可以重定向至有frame的html文件,只有在客户端没有输出时才可以调用forward方法。如果当前页面的缓冲区（buffer）不是空的，那么你在调用forward方法前必须先清空缓冲区。"/"代表相对与web应用路径RequestDispatcher rd = request.getRequestDispatcher("/ooo"); rd.forward(request, response); //提交至http://localhost:8080/Test/ooo RequestDispatcher rd = getServletContext().getRequestDispatcher("/ooo"); rd.forward(request, response); //提交至http://localhost:8080/Test/ooo RequestDispatcher rd =getServletContext().getNamedDispatcher("TestServlet"); //（TestServlet为一个<servlet-name>） rd.forward(request, response); //提交至名为TestServlet的servlet而转发和重定向也叫直接请求转发(Forward)和间接请求转发(Redirect)直接转发方式（Forward），客户端和浏览器只发出一次请求，Servlet、HTML、JSP或其它信息资源，由第二个信息资源响应该请求，在请求对象request中，保存的对象对于每个信息资源是共享的。间接转发方式（Redirect）实际是两次HTTP请求，服务器端在响应第一次请求的时候，让浏览器再向另外一个URL发出请求，从而达到转发的目的。shiro框架初探基本上涉及到用户参与的系统都要进行权限管理，权限管理属于系统安全的范畴，权限管理实现对用户访问系统的控制，按照安全规则或者安全策略控制用户可以访问而且只能访问自己被授权的资源。权限管理包括用户身份认证和授权两部分，简称认证授权。对于需要访问控制的资源用户首先经过身份认证，认证通过后用户具有该资源的访问权限方可访问。身份认证，就是判断一个用户是否为合法用户的处理过程。最常用的简单身份认证方式是系统通过核对用户输入的用户名和口令，看其是否与系统中存储的该用户的用户名和口令一致，来判断用户身份是否正确。对于采用指纹等系统，则出示指纹；对于硬件Key等刷卡系统，则需要刷卡。关键对象：Subject：主体访问系统的用户，主体可以是用户、程序等，进行认证的都称为主体；Principal：身份信息是主体（subject）进行身份认证的标识，标识必须具有唯一性，如用户名、手机号、邮箱地址等，一个主体可以有多个身份，但是必须有一个主身份（Primary Principal）。credential：凭证信息是只有主体自己知道的安全信息，如密码、证书等。授权，即访问控制，控制谁能访问哪些资源。主体进行身份认证后需要分配权限方可访问系统的资源，对于某些资源没有权限是无法访问的。shiro能做什么？认证：验证用户的身份授权：对用户执行访问控制：判断用户是否被允许做某事会话管理：在任何环境下使用 Session API，即使没有 Web 或EJB 容器。加密：以更简洁易用的方式使用加密功能，保护或隐藏数据防止被偷窥Realms：聚集一个或多个用户安全数据的数据源单点登录（SSO）功能。为没有关联到登录的用户启用 "Remember Me“ 服务Shiro 的四大核心部分Authentication(身份验证)：简称为“登录”，即证明用户是谁。Authorization(授权)：访问控制的过程，即决定是否有权限去访问受保护的资源。Session Management(会话管理)：管理用户特定的会话，即使在非 Web 或 EJB 应用程序。Cryptography(加密)：通过使用加密算法保持数据安全shiro的三个核心组件： Subject ：正与系统进行交互的人，或某一个第三方服务。所有 Subject 实例都被绑定到（且这是必须的）一个SecurityManager 上。SecurityManager：Shiro 架构的心脏，用来协调内部各安全组件，管理内部组件实例，并通过它来提供安全管理的各种服务。当 Shiro 与一个 Subject 进行交互时，实质上是幕后的 SecurityManager 处理所有繁重的 Subject 安全操作。Realms ：本质上是一个特定安全的 DAO。当配置 Shiro 时，必须指定至少一个 Realm 用来进行身份验证和/或授权。Shiro 提供了多种可用的 Realms 来获取安全相关的数据。如关系数据库(JDBC)，INI 及属性文件等。可以定义自己 Realm 实现来代表自定义的数据源。三、实验思考及感想系统正在逐步完善，当然问题也出现了，就是了解的东西实在有限，需要查阅很多资料，而且需要不停的踩坑，因为网上很多的资料都是存在问题的，或者说有很多虎头蛇尾的内容，例如在查阅controller入参和返回类型的时候，发现存在返回Map类型，有点意料之外的感觉。并且配置方面因为不是特别深入地了解，只能慢慢摸索，发现想要测试的时候，无法获取js文件，即除了jsp外的静态资源，就需要进行相应的配置，并进行测验。后续会完成shiro框架的加入，以及尝试完成用户头像的上传和收藏列表的实现
一、目录实现通过POST请求进行账号注册通过POST发送账号返回密码了解各种注解作用和POST详情二、实验结果和学习内容首先因为要解析JSON数据以及返回JSON数据，因此我在选择依赖包的时候，在JSON-lib，Jackson和fastjson中选择了Jackson，那么添加依赖，这时候发现之前用的阿里云的maven仓库好像没有最新的版本，于是在下面的网站中查找，换了一个库。AliRepo添加依赖： <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.9.8</version> </dependency> 然后因为我要实现注册用户，以及返回用户密码，那么就要在Dao层和Service层进行修改，然后发现Dao层的两个方法分别是通过用户获取整个用户对象，以及直接向数据库添加用户。然后在Service层增加函数 public String findUser(String username) { User user = userDao.findByUsername(username); if(user != null){ return user.getPassword(); } return "Not Found!"; } 这样就能返回用户密码了。然后在Controller中实现我所要实现的功能。首先是用户注册，这里我返回的一组测试数据。 @ResponseBody @RequestMapping(value = "/trypost2", method = RequestMethod.POST) public List<User> trypost2(@RequestBody User user){ System.out.println(user.getId()+" "+user.getPassword()+" "+user.getPassword()); List<User> list = new ArrayList<User>(); User user1 = new User(); user1.setUsername("00001111"); user1.setPassword("tttttttt"); User user2 = new User(); user2.setUsername("22221111"); user2.setPassword("pppppppp"); list.add(user1); list.add(user2); userServivce.Regist(user); return list; }其中注解@ResponseBody的作用是将controller的方法返回的对象通过适当的转换器转换为指定的格式之后，写入到response对象的body区，通常用来返回JSON数据或者是XML数据，而需要注意的是，在使用此注解之后不会再走试图处理器，而是直接将数据写入到输入流中，他的效果等同于通过response对象输出指定格式的数据。 @RequestMapping("/login") @ResponseBody public User login(User user){ return user; } //User字段：userName pwd //那么在前台接收到的数据为：'{"userName":"xxx","pwd":"xxx"}' //效果等同于如下代码： @RequestMapping("/login") public void login(User user, HttpServletResponse response){ response.getWriter.write(JSONObject.fromObject(user).toString()); } 然后可以进行测试了，因为直接使用curl比较麻烦，因此我下载了一个Postman，用于测试API。效果如下：这是请求：然后得到的结果：说明返回成功了，然后看看服务器的输出：可以看的出成果连接数据库并成功插入了数据，再看看数据库的表的变化：pic7.png产生了变化了说明这个API是成功了的然后是第二个API： @ResponseBody @RequestMapping(value = "/trypost", method = RequestMethod.POST) public List<String> trypost(@RequestParam(value = "username") String username){ List<String> list = new ArrayList<String>(); System.out.println("login: "+username); String pas = userServivce.findUser(username); list.add(pas); return list; } 这里的@RequestParam用于将请求参数区数据映射到功能处理方法的参数上，常用于表格。这里我一开始也像之前一样，通过发送JSON数据来请求，但发现报错了，然后就了解到了POST四种常用数据的区别：application/x-www-form-urlencoded这应该是最常见的 POST 提交数据的方式了。浏览器的原生 form 表单，如果不设置 enctype 属性，那么最终就会以 application/x-www-form-urlencoded 方式提交数据。multipart/form-data这又是一个常见的 POST 数据提交的方式。我们使用表单上传文件时，必须让 form 的 enctyped 等于这个值。这种方式一般用来上传文件，各大服务端语言对它也有着良好的支持。 上面提到的这两种 POST 数据的方式，都是浏览器原生支持的，而且现阶段原生 form 表单也只支持这两种方式。但是随着越来越多的 Web 站点，尤其是 WebApp，全部使用 Ajax 进行数据交互之后，我们完全可以定义新的数据提交方式，给开发带来更多便利。application/jsonapplication/json 这个 Content-Type 作为响应头我们都不陌生。实际上，现在越来越多的人把它作为请求头，用来告诉服务端消息主体是序列化后的 JSON 字符串。由于 JSON 规范的流行，除了低版本 IE 之外的各大浏览器都原生支持 JSON.stringify，服务端语言也都有处理 JSON 的函数，使用 JSON 不会遇上什么麻烦。JSON 格式支持比键值对复杂得多的结构化数据，这一点也很有用。这种方案，可以方便的提交复杂的结构化数据，特别适合 RESTful 的接口。各大抓包工具如 Chrome 自带的开发者工具、Firebug、Fiddler，都会以树形结构展示 JSON 数据，非常友好。text/xmlXML-RPC（XML Remote Procedure Call）。它是一种使用 HTTP 作为传输协议，XML 作为编码方式的远程调用规范。典型的 XML-RPC 请求是这样的：POST http://www.example.com HTTP/1.1 Content-Type: text/xml <methodcall> <methodname>examples.getStateName</methodname> <params> <value><i4>41</i4></value> </params> </methodcall> XML-RPC 协议简单、功能够用，各种语言的实现都有。它的使用也很广泛，如 WordPress 的 XML-RPC Api，搜索引擎的 ping 服务等等。JavaScript 中，也有现成的库支持以这种方式进行数据交互，能很好的支持已有的 XML-RPC 服务。然后更改了requestbody的数据类型之后，就成功了：请求存在的用户：结果：不存在的用户：而且可以看到，请求的头部是有很多数据的：三、实验思考及感想本次试验主要是为了App的登录和其他功能做准备，下一步应该是多用户登录的实现，具体方法还不清楚，但简单的雏形已经完成，包括新闻数据，后期还要加上Kafka消息队列来实现下拉更新的功能，我觉得需要学习的内容还有很多。
一、目录通过SSM框架实现用户登录注册的功能部署到tomcat服务器上二、实验结果和学习内容首先前期准备当然是下载并安装好MySQL，Tomcat8，还有Maven了首先配置好maven之后，通过maven来构建项目：然后构建好项目目录：首先是依赖文件pom.xml，即包含了要用到的包，会下载到maven的本地仓库中：<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>sysu.wangjing</groupId> <artifactId>mySSM</artifactId> <version>1.0-SNAPSHOT</version> <packaging>war</packaging> <name>mySSM Maven Webapp</name> <url>http://www.example.com</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <maven.compiler.source>1.7</maven.compiler.source> <maven.compiler.target>1.7</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.11</version> <scope>test</scope> </dependency> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.7</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.1.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.30</version> <scope>runtime</scope> </dependency> <dependency> <groupId>c3p0</groupId> <artifactId>c3p0</artifactId> <version>0.9.1.2</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.2.8</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.2.2</version> </dependency> <dependency> <groupId>taglibs</groupId> <artifactId>standard</artifactId> <version>1.1.2</version> </dependency> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>2.1.0</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-tx</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-web</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>4.1.4.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-test</artifactId> <version>4.1.7.RELEASE</version> </dependency> </dependencies> <build> <finalName>mySSM</finalName> <pluginManagement> <plugins> <plugin> <artifactId>maven-clean-plugin</artifactId> <version>3.1.0</version> </plugin> <plugin> <artifactId>maven-resources-plugin</artifactId> <version>3.0.2</version> </plugin> <plugin> <artifactId>maven-compiler-plugin</artifactId> <version>3.8.0</version> </plugin> <plugin> <artifactId>maven-surefire-plugin</artifactId> <version>2.22.1</version> </plugin> <plugin> <artifactId>maven-war-plugin</artifactId> <version>3.2.2</version> </plugin> <plugin> <artifactId>maven-install-plugin</artifactId> <version>2.5.2</version> </plugin> <plugin> <artifactId>maven-deploy-plugin</artifactId> <version>2.8.2</version> </plugin> </plugins> </pluginManagement> </build> </project> 然后是UserController.java的实现：package com.mySSM.controller; import javax.servlet.http.HttpSession; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.SessionAttributes; import com.mySSM.entity.User; import com.mySSM.service.UserService; @Controller @RequestMapping("/user") //这里用了@SessionAttributes，可以直接把model中的user(也就key)放入其中 //这样保证了session中存在user这个对象 @SessionAttributes("user") public class UserController { @Autowired private UserService userServivce; //正常访问login页面 @RequestMapping("/login") public String login(){ return "login"; } //表单提交过来的路径 @RequestMapping("/checkLogin") public String checkLogin(User user,Model model){ //调用service方法 user = userServivce.checkLogin(user.getUsername(), user.getPassword()); //若有user则添加到model里并且跳转到成功页面 if(user != null){ model.addAttribute("user",user); return "success"; } return "fail"; } //测试超链接跳转到另一个页面是否可以取到session值 @RequestMapping("/anotherpage") public String hrefpage(){ return "anotherpage"; } //注销方法 @RequestMapping("/outLogin") public String outLogin(HttpSession session){ //通过session.invalidata()方法来注销当前的session session.invalidate(); return "login"; } @RequestMapping("/regist") public String regist(){ return "regist"; } @RequestMapping("/doRegist") public String doRegist(User user,Model model){ System.out.println(user.getUsername()); userServivce.Regist(user); return "success"; } }然后是UserDao的实现：package com.mySSM.dao; import org.apache.ibatis.annotations.Param; import com.mySSM.entity.User; public interface UserDao { /** * 查找用户名和密码 * @param username 登录用户名 * @return */ User findByUsername(String username); /** * 注册用户和密码 */ void registerByUsernameAndPassword(@Param("username")String username, @Param("password")String password); }然后是实体User.java的实现：package com.mySSM.entity; public class User { private String id; private String username; private String password; public String getId() { return id; } public void setId(String id) { this.id = id; } 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; } }接着是service层接口的实现UserService.java：package com.mySSM.service; import com.mySSM.entity.User; //Service层接口 public interface UserService { //检验用户登录 User checkLogin(String username,String password); void Regist(User user); }及其实现UserServiceImpl.java：package com.mySSM.service.impl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import com.mySSM.dao.UserDao; import com.mySSM.entity.User; import com.mySSM.service.UserService; @Service public class UserServiceImpl implements UserService{ @Autowired private UserDao userDao; /* * 检验用户登录业务 * */ public User checkLogin(String username, String password) { User user = userDao.findByUsername(username); if(user != null && user.getPassword().equals(password)){ return user; } return null; } @Override public void Regist(User user) { userDao.registerByUsernameAndPassword(user.getUsername(),user.getPassword()); } }然后在resources文件夹中，分别实现spring-dao.xml，spring-service.xml和spring-web.xml，三个和spring框架相关的配置文件。首先是spring-dao.xml：<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:property-placeholder location="classpath:jdbc.properties"/> <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"> <property name="driverClass" value="${jdbc.driverClass}"/> <property name="jdbcUrl" value="${jdbc.url}"/> <property name="user" value="${jdbc.username}"/> <property name="password" value="${jdbc.password}"/> <property name="maxPoolSize" value="30"/> <property name="minPoolSize" value="10"/> <property name="autoCommitOnClose" value="false"/> <property name="checkoutTimeout" value="1000"/> <property name="acquireRetryAttempts" value="2"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <!-- 扫描entity包 使用别名 com.sy.entity.User ->User --> <property name="typeAliasesPackage" value="com.mySSM.entity"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="sqlSessionFactoryBeanName" value="sqlSessionFactory"/> <property name="basePackage" value="com.mySSM.dao"/> </bean> </beans> 然后是spring-service.xml：<?xml version="1.0" encoding="UTF-8" ?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"> <context:component-scan base-package="com.mySSM.service"/> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource" ref="dataSource"/> </bean> <tx:annotation-driven transaction-manager="transactionManager"/> </beans>最后是spring-web.xml：<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:mvc="http://www.springframework.org/schema/mvc" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd "> <mvc:annotation-driven/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="prefix" value="/WEB-INF/jsp/" /> <property name="suffix" value=".jsp" /> </bean> <context:component-scan base-package="com.mySSM.controller" /> </beans>然后还有几个简单的jsp文件，分别是注册和登录页面。其中登录界面如下：<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> Insert title here 用户名: 密码: 然后要手动连接数据库：然后是配置本地的Tomcat，并且为了方便开发，部署模式选择war exploded：最后Build Project之后就成功部署了，然后可以看到界面成功展示了：三、实验思考及感想这次试验的目的在于先架起一个后端的框架，然后去慢慢地完善它。而这次的内容虽然不多，但很多地方都还是第一次上手，完全不熟悉，网上看到的内容也大多不尽相同，实在是有点摸不着头脑，特别是spring框架的配置文件，不同的人有不同的写法，可以说是五花八门了。然后实际上，功能方面的代码其实还不算多，倒是配置框架所花费的精力要更多，也不知道是好是坏，对于小项目来说，可能会有点杀鸡用牛刀的感觉，也许随着我们项目开发的逐步深入，框架的好处会体现出来吧。
一、目录kafka详解用户登录方案二、实验结果和学习内容Kafka 详解Kafka是一种分布式的，基于发布/订阅的消息系统。主要设计目标如下：以时间复杂度为O(1)的方式提供消息持久化能力，即使对TB级以上数据也能保证常数时间的访问性能高吞吐率。即使在非常廉价的商用机器上也能做到单机支持每秒100K条消息的传输支持Kafka Server间的消息分区，及分布式消费，同时保证每个partition内的消息顺序传输同时支持离线数据处理和实时数据处理而我们的项目目标是完成一款Feed流App，显然消息队列系统是非常重要的。送达保证 消息队列提供的冗余机制保证了消息能被实际的处理，只要一个进程读取了该队列即可。在此基础上，部分消息系统提供了一个”只送达一次”保证。无论有多少进程在从队列中领取数据，每一个消息只能被处理一次。这之所以成为可能，是因为获取一个消息只是”预定”了这个消息，暂时把它移出了队列。除非客户端明确的表示已经处理完了这个消息，否则这个消息会被放回队列中去，在一段可配置的时间之后可再次被处理。顺序保证 在大多使用场景下，数据处理的顺序都很重要。消息队列本来就是排序的，并且能保证数据会按照特定的顺序来处理。部分消息系统保证消息通过FIFO（先进先出）的顺序来处理，因此消息在队列中的位置就是从队列中检索他们的位置。缓冲 在任何重要的系统中，都会有需要不同的处理时间的元素。例如,加载一张图片比应用过滤器花费更少的时间。消息队列通过一个缓冲层来帮助任务最高效率的执行–写入队列的处理会尽可能的快速，而不受从队列读的预备处理的约束。该缓冲有助于控制和优化数据流经过系统的速度。结构Broker Kafka集群包含一个或多个服务器，这种服务器被称为brokerTopic 每条发布到Kafka集群的消息都有一个类别，这个类别被称为topic。（物理上不同topic的消息分开存储，逻辑上一个topic的消息虽然保存于一个或多个broker上但用户只需指定消息的topic即可生产或消费数据而不必关心数据存于何处）Partition parition是物理上的概念，每个topic包含一个或多个partition，创建topic时可指定parition数量。每个partition对应于一个文件夹，该文件夹下存储该partition的数据和索引文件Producer 负责发布消息到Kafka brokerConsumer 消费消息。每个consumer属于一个特定的consumer group（可为每个consumer指定group name，若不指定group name则属于默认的group）。使用consumer high level API时，同一topic的一条消息只能被同一个consumer group内的一个consumer消费，但多个consumer group可同时消费这一消息。kafka的四个核心Api应用程序使用producer API发布消息到1个或多个topic中。应用程序使用consumer API来订阅一个或多个topic,并处理产生的消息。应用程序使用streams API充当一个流处理器,从1个或多个topic消费输入流,并产生一个输出流到1个或多个topic,有效地将输入流转换到输出流。connector API允许构建或运行可重复使用的生产者或消费者,将topic链接到现有的应用程序或数据系统。kafka配置说明ProducerProperties props = new Properties(); props.put("bootstrap.servers", "master:9092,slave1:9092,slave2:9092"); props.put("acks", "all"); props.put("retries", 0); props.put("batch.size", 16384); props.put("key.serializer", StringSerializer.class.getName()); props.put("value.serializer", StringSerializer.class.getName()); KafkaProducer<String, String> producer = new KafkaProducer<String, String>(props);其中：bootstrap.servers： kafka的地址。acks:消息的确认机制，默认值是0；acks=0：如果设置为0，生产者不会等待kafka的响应。acks=1：这个配置意味着kafka会把这条消息写到本地日志文件中，但是不会等待集群中其他机器的成功响应。acks=all：这个配置意味着leader会等待所有的follower同步完成。这个确保消息不会丢失，除非kafka集群中所有机器挂掉。这是最强的可用性保证。retries：配置为大于0的值的话，客户端会在消息发送失败时重新发送。batch.size:当多条消息需要发送到同一个分区时，生产者会尝试合并网络请求。这会提高client和生产者的效率。key.serializer: 键序列化，默认org.apache.kafka.common.serialization.StringDeserializer。value.deserializer:值序列化，默认org.apache.kafka.common.serialization.StringDeserializer。配置好之后，便可以生产数据了：producer.send(new ProducerRecord<String, String>(topic,key,value));其中的参数：topic：消息队列的名称，可以先行在kafka服务中进行创建。如果kafka中并未创建该topic，那么便会自动创建！key：键值，也就是value对应的值，和Map类似。value：要发送的数据，数据格式为String类型的。ConsumerProperties props = new Properties(); props.put("bootstrap.servers", "master:9092,slave1:9092,slave2:9092"); props.put("group.id", GROUPID); props.put("enable.auto.commit", "true"); props.put("auto.commit.interval.ms", "1000"); props.put("session.timeout.ms", "30000"); props.put("max.poll.records", 1000); props.put("auto.offset.reset", "earliest"); props.put("key.deserializer", StringDeserializer.class.getName()); props.put("value.deserializer", StringDeserializer.class.getName()); KafkaConsumer<String, String> consumer = new KafkaConsumer<String, String>(props);其配置如下：group.id：组名 不同组名可以重复消费。例如你先使用了组名A消费了kafka的1000条数据，但是你还想再次进行消费这1000条数据，并且不想重新去产生，那么这里你只需要更改组名就可以重复消费了。enable.auto.commit：是否自动提交，默认为true。auto.commit.interval.ms: 从poll(拉)的回话处理时长。session.timeout.ms：超时时间。max.poll.records：一次最大拉取的条数。auto.offset.reset：消费规则，默认earliest 。earliest：当各分区下有已提交的offset时，从提交的offset开始消费；无提交的offset时，从头开始消费 。latest：当各分区下有已提交的offset时，从提交的offset开始消费；无提交的offset时，消费新产生的该分区下的数据 。none：topic各分区都存在已提交的offset时，从offset后开始消费；只要有一个分区不存在已提交的offset，则抛出异常。首先需要需要先订阅一个topic，也就是指定消费哪一个topic：consumer.subscribe(Arrays.asList(topic));订阅之后，即可从kafka中拉取数据：ConsumerRecords<String, String> msgList=consumer.poll(1000);可以使用for(;;)循环来进行进行监听：for (;;) { msgList = consumer.poll(1000); if (null != msgList && msgList.count() > 0) { for (ConsumerRecord < String, String > record: msgList) { //消费100条就打印 ,但打印的数据不一定是这个规律的 if (messageNo % 100 == 0) { System.out.println(messageNo + "=======receive: key = " + record.key() + ", value = " + record.value() + " offset===" + record.offset()); } //当消费了1000条就退出 if (messageNo % 1000 == 0) { break; } messageNo++; } } else { Thread.sleep(1000); } }用户登录方案一个App首先除了内容就是个人账号了，而用户注册登录的方案是值得探讨的。而登录就需要使用到用户名和密码，为了安全起见，在登录过程中暴漏密码的次数越少越好。首先，为了保证登录的安全性，最起码要使用Https协议HTTPS（Secure Hypertext Transfer Protocol）安全超文本传输协议，是一个安全通信通道，基于HTTP开发，用于在客户计算机和服务器之间交换信息，实际上应用了安全套接字层（SSL）作为HTTP应用层的子层，简单来说它是HTTP的安全版。但是，https协议需要到ca申请证书，一般免费证书很少，需要交费。可以看到所有大型网站，例如京东，淘宝，支付宝，涉及到登录和支付的页面，url都是以https开头，这就意味着，这次通讯是使用https。开放平台的api，例如新浪微博，腾讯等，api请求都是以https开头的。https是业界常用的保证安全性的协议。因此，涉及安全问题的api，都应该使用https协议。虽然，https为了保证安全性，在效率上是比http协议低。基本的用户登录方案在传统的web网站中，可以使用cookie+session来实现用户的登录维护而在app后端，需要避免每次验证用户身份都需要传输用户名和密码，流程如下：此时App已经获取到了token值，为了安全，不在网络上传输token，而使用签名校验（这里使用URL签名）的方式，API请求加上URL签名sign和用户id后如下：test.com/user/update?uid=2&sign=3f1e736bc4ae958ae7e8500b45aefdbb&age=22这样，token就不需要附在URL上了。App后台签名校验流程如下：除此之外，还可以设置时间戳，这样在一定长度的时间之后，URL就失效了，也能提升安全性。三、实验思考及感想本周主要学习了常用的消息队列系统Kafka的架构和使用方法，当然，Kafka可能主要用在集群或者分布式系统中，并且在其中的效果也能体现的更好一些，而我们的项目由于设备限制和能力限制，可能最终只会是单服务器的后台。除此之外，也是项目开头的一个内容，即是App的用户登录方案，和我以前所学的Web开发中运用的方法不同，App中用到了token和sign来进行用户验证，对我来说是一个全新的方案，这是接下来要实现的内容，还有ssm框架的学习。
一、目录服务端常用技术栈服务端开发常用组件信息流服务架构设计Nginx详解二、学习内容数据存储用户资料商品文章图片视频传输消息传递转账上传下载计算数据计算地图导航搜索信息流推荐提供算力语音合成语音实时转写图像内容识别技术栈主要包括基础设施，支撑系统，接入层，逻辑层和存储层，这是C/S架构模式其中基础设施主要是服务运行的环境，大多和硬件，底层系统有关，比较泛化。而支撑系统，主要目的是支撑服务端连续运行，监控错误和漏洞，并收集服务端产生的信息，达到方便维护或者自动维护的效果CI/CDCI/CD 是一种通过在应用开发阶段引入自动化来频繁向客户交付应用的方法。CI/CD 的核心概念是持续集成、持续交付和持续部署。作为一个面向开发和运营团队的解决方案，CI/CD 主要针对在集成新代码时所引发的问题（亦称：“集成地狱”）。具体而言，CI/CD 在整个应用生命周期内（从集成和测试阶段，到交付和部署）引入了持续自动化和持续监控。这些关联的事务通常被统称为“CI/CD 管道”，由开发和运维团队以敏捷方式协同支持。接入层功能：维护与客户端之间的网络连接，管理客户端的网络状态。接收客户端请求，将请求转发到业务层，转发业务层发给客户端的数据。就近接入，负载均衡，优化网络体验。反向代理反向代理是代理服务器的一种。服务器根据客户端的请求，从其关系的一组或多组后端服务器（如Web服务器）上获取资源，然后再将这些资源返回给客户端，客户端只会得知反向代理的IP地址，而不知道在代理服务器后面的服务器簇的存在。而 Nginx 就是性能非常好的反向代理服务器，它可以用来做负载均衡。负载均衡负载均衡（Load Balance）其意思就是分摊到多个操作单元上进行执行，例如Web服务器、FTP服务器、企业关键应用服务器和其它关键任务服务器等，从而共同完成工作任务。负载均衡是高可用网络基础架构的的一个关键组成部分，有了负载均衡，我们通常可以将我们的应用服务器部署多台，然后通过负载均衡将用户的请求分发到不同的服务器用来提高网站、应用、数据库或其他服务的性能以及可靠性。负载均衡算法 ：轮询：为第一个请求选择健康池中的第一个后端服务器，然后按顺序往后依次选择，直到最后一个，然后循环。最小连接：优先选择连接数最少，也就是压力最小的后端服务器，在会话较长的情况下可以考虑采取这种方式。散列：根据请求源的 IP 的散列（hash）来选择要转发的服务器。这种方式可以一定程度上保证特定用户能连接到相同的服务器。如果你的应用需要处理状态而要求用户能连接到和之前相同的服务器，可以考虑采取这种方式。流量清洗对进入的数据流量进行实时监控，及时发现包括DOS攻击在内的异常流量。在不影响正常业务的前提下，清洗掉异常流量。逻辑层作用是根据业务逻辑，处理用户请求，得到结果。设计模式有单体式服务和微服务。单体式模式所有的业务逻辑都集中在一个代码仓库，一个进程里优点是：简单，适合协作人数少，功能较少，且维护和更新较方便缺点是：难以支持高复杂度的系统微服务业务逻辑按功能或类型作划分，分别部署多个服务，使用网络通信交换数据优点是：多人团队协作成本更低，功能复用，系统解耦，故障隔离缺点是：影响性能，系统复杂性大增，对业务流程设计提出了更高的要求，且对网络，基础框架的要求更高RPCRPC（Remote Procedure Call）—远程过程调用，它是一种通过网络从远程计算机程序上请求服务，而不需要了解底层网络技术的协议。RPC协议假定某些传输协议的存在，如TCP或UDP，为通信程序之间携带信息数据。在OSI网络通信模型中，RPC跨越了传输层和应用层。RPC使得开发包括网络分布式多程序在内的应用程序更加容易。常用的RPC框架：gRPCbRPCThriftRPC要解决的问题是：解决分布式系统中，服务之间的调用问题。远程调用时，要能够像本地调用一样方便，让调用者感知不到远程调用的逻辑。实际情况下，RPC很少用到http协议来进行数据传输。而不管使用何种协议进行数据传输，一个完整的RPC过程，都可以用下面这张图来描述：通信协议HTTP：超文本传输协议，数据交换协议的一种万维网数据通信的基础基于 TCP，协议简单HTTPS：TLS 加持的 HTTP提供网站服务器身份认证保护数据交换的隐私以及完整性校验存储层存储层就是DBMS,负责管理对数据库数据的读写和转发。DBMS必须能迅速执行大量数据的更新和检索。现在的主流是关系数据库管理系统(RDBMS)。因此,一般从逻辑层传送到存储层的要求大都使用SQL语言。消息队列“消息队列”是在消息的传输过程中保存消息的容器，其中的“消息”指的是在两台计算机间传送的数据单位。消息可以非常简单，例如只包含文本字符串；也可以更复杂，可能包含嵌入对象。消息被发送到队列中。消息队列管理器在将消息从它的源中继到它的目标时充当中间人。队列的主要目的是提供路由并保证消息的传递；如果发送消息时接收者不可用，消息队列会保留消息，直到可以成功地传递它。其目的是解耦业务逻辑，削峰限流，主要特点是异步处理。常用的有：Kafka：性能很高，消息理论上不会丢RabbitMQ：支持AMQP(Advanced Message Queuing Protocol)ZeroMQ：超轻量的事件组件KVKV（Key-Value）指的是一个解决方案是使用键值（Key-Value）存储数据库，这是一种NoSQL（非关系型数据库）模型，其数据按照键值对的形式进行组织、索引和存储。KV存储非常适合不涉及过多数据关系业务关系的业务数据，同时能有效减少读写磁盘的次数，比SQL数据库存储拥有更好的读写性能。优点是简单，性能高，横向拓展性高。常用的有：Redis：性能高，支持多种数据结构，kv / list / set 等等，有一定的持久化能力Memcached：性能高，简单易用，作为cache使用MongoDB：查询功能强大，性能好关系型数据库支持复杂的数据关系存储和查询常用的是MySQL：性能卓越，服务稳定，开源文件系统分布式文件系统（英语：Distributed file system, DFS），或是网络文件系统（英语：Network File System），是一种允许文件通过网络在多台主机上分享的文件系统，可让多机器上的多用户分享文件和存储空间。 在这样的文件系统中，客户端并非直接访问底层的数据存储区块，而是通过网络，以特定的通信协议和服务器沟通。常见的分布式文件系统有GFS、HDFS、Lustre 、Ceph 、GridFS 、mogileFS、TFS、FastDFS等。各自适用于不同的领域。它们都不是系统级的分布式文件系统，而是应用级的分布式文件存储服务。比较：设计信息流服务由于我们小组所选的项目实现内容为信息流服务，因此需要着重了解如何设计一个信息流服务需要考虑的问题有：文章推荐文章数据存储与读取评论和点赞整体架构如下：符合C/S架构大体流程是：从推荐获取本次要出的文章列表获取文章的信息获取文章的评论和点赞信息打包后返回给客户端Nginx详解Nginx是一款自由的、开源的、高性能的HTTP服务器和反向代理服务器；同时也是一个IMAP、POP3、SMTP代理服务器；nginx可以作为一个HTTP服务器进行网站的发布处理，另外nginx可以作为反向代理进行负载均衡的实现。正向代理：客户端知道服务器端，通过代理端连接服务器端。代理端代理的是服务器端。反向代理：所谓反向，是对正向而言的。服务器端知道客户端，客户端不知道服务器端，通过代理端连接服务器端。代理端代理的是客户端。代理对象刚好相反，所以叫反向代理。Nginx的特点跨平台：Nginx 可以在大多数 Unix like OS编译运行，而且也有Windows的移植版本。配置异常简单，非常容易上手。配置风格跟程序开发一样，神一般的配置非阻塞、高并发连接：数据复制时，磁盘I/O的第一阶段是非阻塞的。官方测试能够支撑5万并发连接，在实际生产环境中跑到2～3万并发连接数.(这得益于Nginx使用了最新的epoll模型)事件驱动：通信机制采用epoll模型，支持更大的并发连接。master/worker结构：一个master进程，生成一个或多个worker进程内存消耗小：处理大并发的请求内存消耗非常小。在3万并发连接下，开启的10个Nginx 进程才消耗150M内存（15M*10=150M）成本低廉：Nginx为开源软件，可以免费使用。而购买F5 BIG-IP、NetScaler等硬件负载均衡交换机则需要十多万至几十万人民币内置的健康检查功能：如果 Nginx Proxy 后端的某台 Web 服务器宕机了，不会影响前端访问。节省带宽：支持 GZIP 压缩，可以添加浏览器本地缓存的 Header 头。稳定性高：用于反向代理，宕机的概率微乎其微Nginx的事件处理机制：对于一个基本的web服务器来说，事件通常有三种类型，网络事件、信号、定时器。 首先，请求的基本过程：建立连接---接收数据---发送数据 。其次，系统底层的操作 ：上述过程（建立连接---接收数据---发送数据）在系统底层就是读写事件。1.如果采用阻塞调用的方式，当读写事件没有准备好时，必然不能够进行读写事件，那么久只好等待，等事件准备好了，才能进行读写事件。那么请求就会被耽搁 。阻塞调用会进入内核等待，cpu就会让出去给别人用了，对单线程的worker来说，显然不合适，当网络事件越多时，都在等待，cpu空闲下来没用，cpu利用率自然上不去了，就没法高并发了。 2.既然没有准备好阻塞调用不行，那么采用非阻塞方式。非阻塞就是，事件，马上返回EAGAIN，告诉你，事件还没准备好，过会再来吧。过了一会，再来检查一下事件，直到事件准备好了为止，在这期间，你就可以先去做其它事情，然后再来看看事件好了没。虽然不阻塞了，但要时不时地过来检查一下事件的状态，虽然可以做更多的事情了，但带来的开销也是不小的 小结：非阻塞通过不断检查事件的状态来判断是否进行读写操作，这样带来的开销很大。 3.因此才有了异步非阻塞的事件处理机制。具体到系统调用就是像select/poll/epoll/kqueue这样的系统调用。他们提供了一种机制，可以同时监控多个事件，调用他们是阻塞的，但可以设置超时时间，在超时时间之内，如果有事件准备好了，就返回。这种机制解决了上面两个问题。 以epoll为例：当事件没有准备好时，就放入epoll(队列)里面。如果有事件准备好了，那么就去处理；如果事件返回的是EAGAIN，那么继续将其放入epoll里面。从而，只要有事件准备好了，我们就去处理她，只有当所有时间都没有准备好时，才在epoll里面等着。这样 ，就可以并发处理大量的并发了，当然，这里的并发请求，是指未处理完的请求，线程只有一个，所以同时能处理的请求当然只有一个，只是在请求间进行不断地切换而已，切换也是因为异步事件未准备好，而主动让出的。这里的切换是没有任何代价，可以理解为循环处理多个准备好的事件。 4.与多线程的比较：与多线程相比，这种事件处理方式是有很大的优势的，不需要创建线程，每个请求占用的内存也很少，没有上下文切换，事件处理非常的轻量级。并发数再多也不会导致无谓的资源浪费（上下文切换）。小结：通过异步非阻塞的事件处理机制，Nginx实现由进程循环处理多个准备好的事件，从而实现高并发和轻量级。Nginx内部模型：nginx是以多进程的方式来工作的，当然nginx也是支持多线程的方式的,只是主流的方式还是多进程的方式，也是nginx的默认方式。nginx采用多进程的方式有诸多好处。nginx在启动后，会有一个master进程和多个worker进程。master进程主要用来管理worker进程，包含：接收来自外界的信号，向各worker进程发送信号，监控 worker进程的运行状态,当worker进程退出后(异常情况下)，会自动重新启动新的worker进程。而基本的网 络事件，则是放在worker进程中来处理了 。多个worker进程之间是对等的，他们同等竞争来自客户端的请求，各进程互相之间是独立的 。一个请求，只可能在一个worker进程中处理，一个worker进程，不可能处理其它进程的请求。 worker进程的个数是可以设置的，一般我们会设置与机器cpu核数一致，这里面的原因与nginx的进程模型以及事件处理模型是分不开的 。Master接收到信号以后怎样进行处理（./nginx -s reload ）?首先master进程在接到信号后，会先重新加载配置文件，然后再启动新的进程，并向所有老的进程发送信号，告诉他们可以光荣退休了。新的进程在启动后，就开始接收新的请求，而老的进程在收到来自 master的信号后，就不再接收新的请求，并且在当前进程中的所有未处理完的请求处理完成后，再退出 .worker进程又是如何处理请求的呢？前面有提到，worker进程之间是平等的，每个进程，处理请求的机会也是一样的。当提供80端口的http服务时，一个连接请求过来，每个进程都有可能处理这个连接。首先，每个worker进程都是从master 进程fork(分配)过来，在master进程里面，先建立好需要listen的socket之后，然后再fork出多个worker进程，这样每个worker进程都可以去accept这个socket(当然不是同一个socket，只是每个进程的这个socket会监控在同一个ip地址与端口，这个在网络协议里面是允许的)。一般来说，当一个连接进来后，所有在accept在这个socket上面的进程，都会收到通知，而只有一个进程可以accept这个连接，其它的则accept失败，这是所谓的惊群现象。当然，nginx也不会视而不见，所以nginx提供了一个accept_mutex这个东西，从名字上，可以看这是一个加在accept上的一把共享锁。有了这把锁之后，同一时刻，就只会有一个进程在accpet连接，这样就不会有惊群问题了。accept_mutex是一个可控选项，我们可以显示地关掉，默认是打开的。当一个worker进程在accept这个连接之后，就开始读取请求，解析请求，处理请求，产生数据后，再返回给客户端，最后才断开连接，这样一个完整的请求就是这样的了。因此可以看到，一个请求，完全由worker进程来处理，而且只在一个worker进程中处理。nginx采用这种进程模型有什么好处呢？采用独立的进程，可以让互相之间不会影响，一个进程退出后，其它进程还在工作，服务不会中断，master进程则很快重新启动新的worker进程。而worker进程的异常退出，意味着程序有bug了，异常退出，会导致当前worker上的所有请求失败，不过不会影响到所有请求，所以降低了风险。按理来说，nginx采用多worker的方式来处理请求，每个worker里面只有一个主线程，那能够处理的并发数很有限，多少个worker就能处理多少个并发，何来高并发呢？并非如此，nginx采用了异步非阻塞的方式来处理请求，也就是说，nginx是可以同时处理成千上万个请求的 .对于IIS服务器每个请求会独占一个工作线程，当并发数上到几千时，就同时有几千的线程在处理请求了。这对操作系统来说，是个不小的挑战，线程带来的内存占用非常大，线程的上下文切换带来的cpu开销很大，自然性能就上不去了，而这些开销完全是没有意义的。之前说过，推荐设置worker的个数为cpu的核数，在这里就很容易理解了，更多的worker数，只会导致进程来竞争cpu资源了，从而带来不必要的上下文切换。而且，nginx为了更好的利用多核特性，提供了cpu亲缘性的绑定选项，我们可以将某一个进程绑定在某一个核上，这样就不会因为进程的切换带来cache的失效。Nginx是如何处理一个请求首先，nginx在启动时，会解析配置文件，得到需要监听的端口与ip地址，然后在nginx的master进程里面，先初始化好这个监控的socket(创建socket，设置addrreuse等选项，绑定到指定的ip地址端口，再listen)，然后再fork(一个现有进程可以调用fork函数创建一个 新进程。由fork创建的新进程被称为子进程 )出多个子进程出来，然后子进程会竞争accept新的连接。此时，客户端就可以向nginx发起连接了。当客户端与nginx进行三次握手，与nginx建立好一个连接后，此时，某一个子进程会accept成功，得到这个建立好的连接的 socket，然后创建nginx对连接的封装，即ngx_connection_t结构体。接着，设置读写事件处理函数并添加读写事件来与客户端进行数据的交换。最后，nginx或客户端来主动关掉连接。当然，nginx也是可以作为客户端来请求其它server的数据的（如upstream模块），此时，与其它server创建的连接，也封装在ngx_connection_t中。作为客户端，nginx先获取一个ngx_connection_t结构体，然后创建socket，并设置socket的属性（ 比如非阻塞）。然后再通过添加读写事件，调用connect/read/write来调用连接，最后关掉连接，并释放ngx_connection_t。nginx在实现时，是通过一个连接池来管理的，每个worker进程都有一个独立的连接池，连接池的大小是worker_connections。这里的连接池里面保存的其实不是真实的连接，它只是一个worker_connections大小的一个ngx_connection_t结构的数组。并且，nginx会通过一个链表free_connections来保存所有的空闲ngx_connection_t，每次获取一个连接时，就从空闲连接链表中获取一个，用完后，再放回空闲连接链表里面。 在这里，很多人会误解worker_connections这个参数的意思，认为这个值就是nginx所能建立连接的最大值。其实不然，这个值是表示每个worker进程所能建立连接的最大值，所以，一个nginx能建立的最大连接数，应该是worker_connections * worker_processes。当然 ，这里说的是最大连接数，对于HTTP请求本地资源来说，能够支持的最大并发数量是worker_connections * worker_processes，而如果是HTTP作为反向代理来说，最大并发数量应该是worker_connections * worker_processes/2。因为作为反向代理服务器，每个并发会建立与客户端的连接和与后端服务的连接，会占用两个连接。